diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index 8087585..a2c0771 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -5,9 +5,12 @@ use tui::{ widgets::{ListItem, ListState}, }; +use super::Header; + #[derive(Debug, Clone)] pub struct StatefulList { pub state: ListState, + // HASH MAP! pub items: Vec, } @@ -114,18 +117,18 @@ impl State { _ => Color::Red, } } - pub fn as_text(&self) -> &'static str { - match self { - Self::Dead => "dead", - Self::Exited => "exited", - Self::Paused => "paused", - Self::Removing => "removing", - Self::Restarting => "restarting", - Self::Running => "running", - Self::Unknown => "unknown", - } - } - } + pub fn as_text(&self) -> &'static str { + match self { + Self::Dead => "dead", + Self::Exited => "exited", + Self::Paused => "paused", + Self::Removing => "removing", + Self::Restarting => "restarting", + Self::Running => "running", + Self::Unknown => "unknown", + } + } +} impl From<&str> for State { fn from(input: &str) -> Self { @@ -420,31 +423,31 @@ impl ContainerItem { /// Container information panel headings + widths, for nice pretty formatting #[derive(Debug)] pub struct Columns { - pub state: (String, usize), - pub status: (String, usize), - pub cpu: (String, usize), - pub mem: (String, usize), - pub id: (String, usize), - pub name: (String, usize), - pub image: (String, usize), - pub net_rx: (String, usize), - pub net_tx: (String, usize), + pub state: (Header, usize), + pub status: (Header, usize), + pub cpu: (Header, usize), + pub mem: (Header, usize), + pub id: (Header, usize), + pub name: (Header, usize), + pub image: (Header, usize), + pub net_rx: (Header, usize), + pub net_tx: (Header, usize), } impl Columns { - //. (Column titles, minimum header string length) + // (Column titles, minimum header string length) pub fn new() -> Self { Self { - state: (String::from("state"), 11), - status: (String::from("status"), 16), + state: (Header::State, 11), + status: (Header::Status, 16), // 7 to allow for "100.00%" - cpu: (String::from("cpu"), 7), - mem: (String::from("memory/limit"), 12), - id: (String::from("id"), 8), - name: (String::from("name"), 4), - image: (String::from("image"), 5), - net_rx: (String::from("↓ rx"), 5), - net_tx: (String::from("↑ tx"), 5), + cpu: (Header::Cpu, 7), + mem: (Header::Memory, 12), + id: (Header::Id, 8), + name: (Header::Name, 4), + image: (Header::Image, 5), + net_rx: (Header::Rx, 5), + net_tx: (Header::Tx, 5), } } } diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 687ddb6..52de37c 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -1,4 +1,5 @@ use bollard::models::ContainerSummary; +use core::fmt; use std::time::{SystemTime, UNIX_EPOCH}; use tui::widgets::ListItem; @@ -16,31 +17,58 @@ pub struct AppData { pub containers: StatefulList, pub init: bool, pub show_error: bool, - // todo - sort_by: Header + sorted_by: Option<(Header, SortedOrder)>, } #[derive(Debug, Clone)] -pub enum SortedOrder{ - Asc, - Desc +pub enum SortedOrder { + Asc, + Desc, } -#[derive(Debug, Clone)] -pub enum Header{ - State, - Status, - Cpu, - Memory, - Id, - Name, - Image, - Rx, - Tx +#[derive(Debug, Clone, PartialEq)] +pub enum Header { + State, + Status, + Cpu, + Memory, + Id, + Name, + Image, + Rx, + Tx, } +/// Convert errors into strings to display +impl fmt::Display for Header { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let disp = match self { + Self::State => "state", + Self::Status => "status", + Self::Cpu => "cpu", + Self::Memory => "memory/limit", + Self::Id => "id", + Self::Name => "name", + Self::Image => "image", + Self::Rx => "↓ rx", + Self::Tx => "↑ tx", + }; + write!(f, "{:>x$}", disp, x = f.width().unwrap_or(1)) + } +} impl AppData { + pub fn get_sorted(&self) -> Option<(Header, SortedOrder)> { + self.sorted_by.clone() + } + + /// Change the sorted order, also set the selected pointer! + pub fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) { + self.sorted_by = x; + let id = self.get_selected_container_id(); + self.sort_containers(); + self.containers.state.select(self.containers.items.iter().position(|i| Some(i.id.to_owned()) == id)); + } /// Generate a default app_state pub fn default(args: CliArgs) -> Self { Self { @@ -50,7 +78,7 @@ impl AppData { init: false, logs_parsed: false, show_error: false, - sort_by: Header::Memory, + sorted_by: Some((Header::Memory, SortedOrder::Asc)), } } @@ -141,85 +169,97 @@ impl AppData { output } - - pub fn sort_containers(&mut self, so: SortedOrder) { - - // State, - // Status, - // Cpu, - // Memory, - // Id, - // Name, - // Image, - // Rx, - // Tx - match self.sort_by { - Header::State => { - match so { - SortedOrder::Asc => self.containers.items.sort_by(|a,b|a.state.as_text().cmp(b.state.as_text())), - SortedOrder::Desc => self.containers.items.sort_by(|a,b|b.state.as_text().cmp(a.state.as_text())), - } - - }, - Header::Status => { - match so { - SortedOrder::Asc => self.containers.items.sort_by(|a,b|a.status.cmp(&b.status)), - SortedOrder::Desc => self.containers.items.sort_by(|a,b|b.status.cmp(&a.status)), - } - }, - - Header::Status => { - match so { - SortedOrder::Asc => self.containers.items.sort_by(|a,b|a.status.cmp(&b.status)), - SortedOrder::Desc => self.containers.items.sort_by(|a,b|b.status.cmp(&a.status)), - } - }, - - Header::Cpu => { - match so { - SortedOrder::Desc => - self.containers.items.sort_by(|a,b|a.cpu_stats.back().cmp(&b.cpu_stats.back())), - SortedOrder::Asc => self.containers.items.sort_by(|a,b|b.cpu_stats.back().cmp(&a.cpu_stats.back())) - } - }, - - Header::Memory => { - match so { - SortedOrder::Desc => - self.containers.items.sort_by(|a,b|a.mem_stats.back().cmp(&b.mem_stats.back())), - SortedOrder::Asc => self.containers.items.sort_by(|a,b|b.mem_stats.back().cmp(&a.mem_stats.back())) - } - }, - Header::Image => { - match so { - SortedOrder::Asc => self.containers.items.sort_by(|a,b|a.image.cmp(&b.image)), - SortedOrder::Desc => self.containers.items.sort_by(|a,b|b.image.cmp(&a.image)), - } - }, - Header::Name => { - match so { - SortedOrder::Asc => self.containers.items.sort_by(|a,b|a.name.cmp(&b.name)), - SortedOrder::Desc => self.containers.items.sort_by(|a,b|b.name.cmp(&a.name)), - } - }, - _ => () - } - } - - - - // match so { - // SortedOrder::Asc => self.containers.items.sort_by(|a,b|b.name.cmp(&a.name)), - // SortedOrder::Desc => self.containers.items.sort_by(|a,b|a.name.cmp(&b.name)) - // } - // } - - pub fn sort_by_id(&mut self, so: SortedOrder) { - match so { - SortedOrder::Asc => self.containers.items.sort_by(|a,b|b.id.cmp(&a.id)), - SortedOrder::Desc => self.containers.items.sort_by(|a,b|a.id.cmp(&b.id)) - } - } + /// Sort the containers vec, based on a heading, either ascending or descending + pub fn sort_containers(&mut self) { + if let Some((head, so)) = self.sorted_by.as_ref() { + match head { + Header::State => match so { + SortedOrder::Asc => self + .containers + .items + .sort_by(|a, b| a.state.as_text().cmp(b.state.as_text())), + SortedOrder::Desc => self + .containers + .items + .sort_by(|a, b| b.state.as_text().cmp(a.state.as_text())), + }, + Header::Status => match so { + SortedOrder::Asc => self + .containers + .items + .sort_by(|a, b| a.status.cmp(&b.status)), + SortedOrder::Desc => self + .containers + .items + .sort_by(|a, b| b.status.cmp(&a.status)), + }, + Header::Status => match so { + SortedOrder::Asc => self + .containers + .items + .sort_by(|a, b| a.status.cmp(&b.status)), + SortedOrder::Desc => self + .containers + .items + .sort_by(|a, b| b.status.cmp(&a.status)), + }, + Header::Cpu => match so { + SortedOrder::Desc => self + .containers + .items + .sort_by(|a, b| a.cpu_stats.back().cmp(&b.cpu_stats.back())), + SortedOrder::Asc => self + .containers + .items + .sort_by(|a, b| b.cpu_stats.back().cmp(&a.cpu_stats.back())), + }, + Header::Memory => match so { + SortedOrder::Desc => self + .containers + .items + .sort_by(|a, b| a.mem_stats.back().cmp(&b.mem_stats.back())), + SortedOrder::Asc => self + .containers + .items + .sort_by(|a, b| b.mem_stats.back().cmp(&a.mem_stats.back())), + }, + Header::Id => match so { + SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.id.cmp(&b.id)), + SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.id.cmp(&a.id)), + }, + Header::Image => match so { + SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.image.cmp(&b.image)), + SortedOrder::Desc => { + self.containers.items.sort_by(|a, b| b.image.cmp(&a.image)) + } + }, + Header::Name => match so { + SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.name.cmp(&b.name)), + SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.name.cmp(&a.name)), + }, + Header::Rx => match so { + SortedOrder::Asc => self + .containers + .items + .sort_by(|a, b| a.net_rx.cmp(&b.net_rx)), + SortedOrder::Desc => self + .containers + .items + .sort_by(|a, b| b.net_rx.cmp(&a.net_rx)), + }, + Header::Tx => match so { + SortedOrder::Asc => self + .containers + .items + .sort_by(|a, b| a.net_tx.cmp(&b.net_tx)), + SortedOrder::Desc => self + .containers + .items + .sort_by(|a, b| b.net_tx.cmp(&a.net_tx)), + }, + } + } + } /// Find the index of the currently selected single log line pub fn get_selected_log_index(&self) -> Option { diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index 4ceeefa..5bfde67 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -1,6 +1,7 @@ use bollard::{ container::{ListContainersOptions, LogsOptions, StartContainerOptions, Stats, StatsOptions}, - Docker, models::ContainerSummary, + models::ContainerSummary, + Docker, }; use futures_util::{future::join_all, StreamExt}; use parking_lot::Mutex; @@ -8,7 +9,7 @@ use std::sync::Arc; use tokio::{sync::mpsc::Receiver, task::JoinHandle}; use crate::{ - app_data::{AppData, DockerControls, SortedOrder, Header}, + app_data::{AppData, DockerControls, Header, SortedOrder}, app_error::AppError, parse_args::CliArgs, ui::GuiState, @@ -123,25 +124,25 @@ impl DockerData { } } - // pub fn sort_containers(i: &mut [ContainerSummary], so: SortedOrder, header: Header) -> &[ContainerSummary] { - // match header { - // Header::State => { - // match so { - // SortedOrder::Asc => i.sort_by(|a,b|b.state.cmp(&a.state)), - // SortedOrder::Desc => i.sort_by(|a,b|a.state.cmp(&b.state)), - // } + // pub fn sort_containers(i: &mut [ContainerSummary], so: SortedOrder, header: Header) -> &[ContainerSummary] { + // match header { + // Header::State => { + // match so { + // SortedOrder::Asc => i.sort_by(|a,b|b.state.cmp(&a.state)), + // SortedOrder::Desc => i.sort_by(|a,b|a.state.cmp(&b.state)), + // } - // }, - // Header::Image => { - // match so { - // SortedOrder::Asc => i.sort_by(|a,b|b.image.cmp(&a.image)), - // SortedOrder::Desc => i.sort_by(|a,b|a.image.cmp(&b.image)), - // } - // }, - // _ => () - // } - // i - // } + // }, + // Header::Image => { + // match so { + // SortedOrder::Asc => i.sort_by(|a,b|b.image.cmp(&a.image)), + // SortedOrder::Desc => i.sort_by(|a,b|a.image.cmp(&b.image)), + // } + // }, + // _ => () + // } + // i + // } /// Get all current containers, handle into ContainerItem in the app_data struct rather than here /// Just make sure that items sent are guaranteed to have an id @@ -163,13 +164,12 @@ impl DockerData { .filter(|i| i.id.is_some()) .for_each(|c| output.push(c.to_owned())); - - // containers.so - // let a = Self::sort_containers(&mut output, SortedOrder::Asc, Header::State); + // containers.so + // let a = Self::sort_containers(&mut output, SortedOrder::Asc, Header::State); self.app_data.lock().update_containers(&output); - // self.app_data.lock().sort_containers(SortedOrder::Asc, Header::State); + // self.app_data.lock().sort_containers(SortedOrder::Asc, Header::State); output .iter() @@ -241,7 +241,6 @@ impl DockerData { }; self.update_all_container_stats(&all_ids).await; - } /// Animate the loading icon diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index ec613f0..d7e3260 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -18,7 +18,7 @@ use tui::layout::Rect; mod message; use crate::{ - app_data::{AppData, DockerControls}, + app_data::{AppData, DockerControls, Header, SortedOrder}, app_error::AppError, docker_data::DockerMessage, ui::{GuiState, SelectablePanel}, @@ -115,6 +115,20 @@ impl InputHandler { self.mouse_capture = !self.mouse_capture; } + fn sort(&self, header: Header) { + let mut locked_data = self.app_data.lock(); + if let Some((s, h)) = locked_data.get_sorted().as_ref() { + match (s, h) { + (header, SortedOrder::Asc) => { + locked_data.set_sorted(Some((header.to_owned(), SortedOrder::Desc))) + } + _ => locked_data.set_sorted(Some((header, SortedOrder::Asc))), + } + } else { + locked_data.set_sorted(Some((header, SortedOrder::Asc))) + } + } + /// Handle any keyboard button events async fn button_press(&mut self, key_code: KeyCode) { let show_error = self.app_data.lock().show_error; @@ -140,6 +154,15 @@ impl InputHandler { } } else { match key_code { + KeyCode::Char('1') => self.sort(Header::State), + KeyCode::Char('2') => self.sort(Header::Status), + KeyCode::Char('3') => self.sort(Header::Cpu), + KeyCode::Char('4') => self.sort(Header::Memory), + KeyCode::Char('5') => self.sort(Header::Id), + KeyCode::Char('6') => self.sort(Header::Image), + KeyCode::Char('7') => self.sort(Header::Name), + KeyCode::Char('8') => self.sort(Header::Rx), + KeyCode::Char('9') => self.sort(Header::Tx), KeyCode::Char('q') => self.is_running.store(false, Ordering::SeqCst), KeyCode::Char('h') => self.gui_state.lock().show_help = true, KeyCode::Char('m') => self.m_button(), diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 365cd9b..bb6d263 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -14,7 +14,7 @@ use tui::{ Frame, }; -use crate::app_data::{SortedOrder, Header}; +use crate::app_data::{Header, SortedOrder}; use crate::{ app_data::{AppData, ByteStats, Columns, CpuStats, State, Stats}, app_error::AppError, @@ -124,9 +124,6 @@ pub fn draw_containers( widths: &Columns, ) { let block = generate_block(app_data, area, gui_state, SelectablePanel::Containers); - app_data.lock().sort_containers(SortedOrder::Asc); - - let items = app_data .lock() .containers @@ -361,12 +358,39 @@ pub fn draw_heading_bar( has_containers: bool, loading_icon: String, info_visible: bool, + sorted_by: Option<(Header, SortedOrder)>, ) { let block = || Block::default().style(Style::default().bg(Color::Magenta).fg(Color::Black)); f.render_widget(block(), area); - let mut column_headings = format!( + let aaa = |x: &Header| { + let mut output = ""; + if let Some((a, b)) = sorted_by.as_ref() { + if x == a { + output = match b { + SortedOrder::Asc => "A", + SortedOrder::Desc => "B", + }; + }; + }; + output + }; + + // need to split this into blocks, and put each block in the split block, and set color to white if is selected + // then just put in the split in a horizontal fashion, with a width equal to widtrh, or char count? + // let white = "\x1b[37m"; + // let reset = "\x1b[0m"; + + // let mut column_headings = format!( + // " {}{:>width$}{}", + // loading_icon, + // columns.state.0, + // width = columns.state.1, + // ); + + // Each + let mut column_headings = format!( " {}{:>width$}", loading_icon, columns.state.0, @@ -381,6 +405,9 @@ pub fn draw_heading_bar( ) .as_str(), ); + + // Get selected and sorted + // Maybe each heading needs to be its own boock column_headings .push_str(format!("{}{:>width$}", MARGIN, columns.cpu.0, width = columns.cpu.1).as_str()); column_headings @@ -425,16 +452,13 @@ pub fn draw_heading_bar( ); 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 info_text = format!("( h ) {} help {}", suffix, MARGIN); + let info_width = info_text.chars().count() as u16; + let x = area.width - info_width; + let column_width = if x > 0 { x } else { 1 }; let splits = if has_containers { - vec![ - Constraint::Min(column_width as u16), - Constraint::Min(info_width as u16), - ] + vec![Constraint::Min(column_width), Constraint::Min(info_width)] } else { vec![Constraint::Percentage(100)] }; @@ -482,6 +506,7 @@ pub fn draw_help_box(f: &mut Frame<'_, B>) { .push_str("\n ( ↑ ↓ ) or ( j k ) or (PgUp PgDown) or (Home End) to change selected line"); help_text.push_str("\n ( enter ) to send docker container commands"); help_text.push_str("\n ( h ) to toggle this help information"); + help_text.push_str("\n ( 1 - 9 ) order headers"); help_text.push_str( "\n ( m ) to toggle mouse capture - if disabled, text on screen can be selected & copied", ); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 8a41d72..b937dcf 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -157,6 +157,7 @@ fn ui( let show_help = gui_state.lock().show_help; let info_text = gui_state.lock().info_box_text.clone(); let loading_icon = gui_state.lock().get_loading(); + let sorted_by = app_data.lock().get_sorted(); let whole_layout = Layout::default() .direction(Direction::Vertical) @@ -214,6 +215,7 @@ fn ui( has_containers, loading_icon, show_help, + sorted_by, ); // only draw charts if there are containers