Merge branch 'feat/sort_by' into dev, closes #3
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 288 KiB After Width: | Height: | Size: 426 KiB |
@@ -38,6 +38,19 @@ rm oxker_linux_x86_64.tar.gz oxker
|
||||
|
||||
```oxker```
|
||||
|
||||
In application controls
|
||||
| button| result|
|
||||
|--|--|
|
||||
| ```( tab )``` or ```( shift+tab )``` | Change panel, clicking on a panel also changes the selected panel|
|
||||
| ```( ↑ ↓ )``` or ```( j k )``` or ```(PgUp PgDown)``` or ```(Home End)```| Change selected line in selected panel, mouse scroll also changes selected line |
|
||||
| ```( enter )```| execute selected docker command|
|
||||
| ```( 1-9 )``` | sort containers by heading, clicking on headings also sorts the selected column |
|
||||
| ```( 0 )``` | stop sorting |
|
||||
| ```( h )``` | Show help menu |
|
||||
| ```( m )``` | toggle mouse capture - if disabled, text on screen can be selected|
|
||||
| ```( q )``` | to quit at any time |
|
||||
|
||||
|
||||
available command line arguments
|
||||
| argument|result|
|
||||
|--|--|
|
||||
|
||||
+1
-1
@@ -183,7 +183,6 @@ cargo_test () {
|
||||
release_flow() {
|
||||
check_git
|
||||
get_git_remote_url
|
||||
cargo fmt
|
||||
cargo_test
|
||||
cd "${CWD}" || error_close "Can't find ${CWD}"
|
||||
check_tag
|
||||
@@ -195,6 +194,7 @@ release_flow() {
|
||||
ask_changelog_update
|
||||
git checkout -b "$RELEASE_BRANCH"
|
||||
update_version_number_in_files
|
||||
cargo fmt
|
||||
git add .
|
||||
git commit -m "chore: release $NEW_TAG_WITH_V"
|
||||
|
||||
|
||||
@@ -5,9 +5,12 @@ use tui::{
|
||||
widgets::{ListItem, ListState},
|
||||
};
|
||||
|
||||
use super::Header;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StatefulList<T> {
|
||||
pub state: ListState,
|
||||
// HASH MAP!
|
||||
pub items: Vec<T>,
|
||||
}
|
||||
|
||||
@@ -81,6 +84,7 @@ impl<T> StatefulList<T> {
|
||||
}
|
||||
|
||||
/// States of the container
|
||||
// / impl ord
|
||||
#[derive(Clone, Debug, PartialEq, PartialOrd)]
|
||||
pub enum State {
|
||||
Dead,
|
||||
@@ -92,6 +96,17 @@ pub enum State {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
// impl Ord for State {
|
||||
// fn cmp(&self, other: &Self) -> Ordering {
|
||||
// match (self, other) {
|
||||
// (Self::Dead)
|
||||
// // (_, Foo::B) => Ordering::Less,
|
||||
// // (Foo::A { val: l }, Foo::A { val: r }) => l.cmp(&r),
|
||||
// // (Foo::B, _) => Ordering::Greater,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
impl State {
|
||||
pub fn get_color(&self) -> Color {
|
||||
match self {
|
||||
@@ -102,6 +117,18 @@ impl State {
|
||||
_ => Color::Red,
|
||||
}
|
||||
}
|
||||
// Dirty way to create order for the state, rather than impl Ord
|
||||
pub fn order(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Running => "a",
|
||||
Self::Paused => "b",
|
||||
Self::Restarting => "c",
|
||||
Self::Removing => "d",
|
||||
Self::Exited => "e",
|
||||
Self::Dead => "f",
|
||||
Self::Unknown => "g",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for State {
|
||||
@@ -397,31 +424,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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use bollard::models::ContainerSummary;
|
||||
use core::fmt;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tui::widgets::ListItem;
|
||||
|
||||
@@ -16,9 +17,63 @@ pub struct AppData {
|
||||
pub containers: StatefulList<ContainerItem>,
|
||||
pub init: bool,
|
||||
pub show_error: bool,
|
||||
sorted_by: Option<(Header, SortedOrder)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum SortedOrder {
|
||||
Asc,
|
||||
Desc,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
|
||||
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 container state to match new order
|
||||
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 {
|
||||
@@ -28,6 +83,7 @@ impl AppData {
|
||||
init: false,
|
||||
logs_parsed: false,
|
||||
show_error: false,
|
||||
sorted_by: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +174,88 @@ impl AppData {
|
||||
output
|
||||
}
|
||||
|
||||
/// 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::Desc => self
|
||||
.containers
|
||||
.items
|
||||
.sort_by(|a, b| a.state.order().cmp(b.state.order())),
|
||||
SortedOrder::Asc => self
|
||||
.containers
|
||||
.items
|
||||
.sort_by(|a, b| b.state.order().cmp(a.state.order())),
|
||||
},
|
||||
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::Asc => self
|
||||
.containers
|
||||
.items
|
||||
.sort_by(|a, b| a.cpu_stats.back().cmp(&b.cpu_stats.back())),
|
||||
SortedOrder::Desc => self
|
||||
.containers
|
||||
.items
|
||||
.sort_by(|a, b| b.cpu_stats.back().cmp(&a.cpu_stats.back())),
|
||||
},
|
||||
Header::Memory => match so {
|
||||
SortedOrder::Asc => self
|
||||
.containers
|
||||
.items
|
||||
.sort_by(|a, b| a.mem_stats.back().cmp(&b.mem_stats.back())),
|
||||
SortedOrder::Desc => 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<usize> {
|
||||
let mut output = None;
|
||||
|
||||
@@ -144,6 +144,10 @@ impl DockerData {
|
||||
.for_each(|c| output.push(c.to_owned()));
|
||||
|
||||
self.app_data.lock().update_containers(&output);
|
||||
|
||||
let current_sort = self.app_data.lock().get_sorted();
|
||||
self.app_data.lock().set_sorted(current_sort);
|
||||
|
||||
output
|
||||
.iter()
|
||||
.filter_map(|i| {
|
||||
@@ -187,6 +191,11 @@ impl DockerData {
|
||||
output
|
||||
}
|
||||
|
||||
// async fn stop(&self) {
|
||||
// self.docker.
|
||||
|
||||
// }
|
||||
|
||||
/// Update all logs, spawn each container into own tokio::spawn thread
|
||||
async fn init_all_logs(&mut self, all_ids: &[(bool, String)]) {
|
||||
let mut handles = vec![];
|
||||
|
||||
@@ -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,18 @@ impl InputHandler {
|
||||
self.mouse_capture = !self.mouse_capture;
|
||||
}
|
||||
|
||||
/// Sort containers based on a given header, switch asc to desc if already sorted, else always desc
|
||||
fn sort(&self, header: Header) {
|
||||
let mut output = Some((header.to_owned(), SortedOrder::Desc));
|
||||
let mut locked_data = self.app_data.lock();
|
||||
if let Some((h, order)) = locked_data.get_sorted().as_ref() {
|
||||
if &SortedOrder::Desc == order && h == &header {
|
||||
output = Some((header, SortedOrder::Asc))
|
||||
}
|
||||
}
|
||||
locked_data.set_sorted(output)
|
||||
}
|
||||
|
||||
/// Handle any keyboard button events
|
||||
async fn button_press(&mut self, key_code: KeyCode) {
|
||||
let show_error = self.app_data.lock().show_error;
|
||||
@@ -140,11 +152,27 @@ impl InputHandler {
|
||||
}
|
||||
} else {
|
||||
match key_code {
|
||||
KeyCode::Char('0') => self.app_data.lock().set_sorted(None),
|
||||
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::Name),
|
||||
KeyCode::Char('7') => self.sort(Header::Image),
|
||||
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(),
|
||||
KeyCode::Tab => self.gui_state.lock().next_panel(),
|
||||
KeyCode::BackTab => self.gui_state.lock().previous_panel(),
|
||||
KeyCode::Tab => {
|
||||
// TODO if no containers, skip controls panel
|
||||
self.gui_state.lock().next_panel();
|
||||
}
|
||||
KeyCode::BackTab => {
|
||||
// TODO if no containers, skip controls panel
|
||||
self.gui_state.lock().previous_panel();
|
||||
}
|
||||
KeyCode::Home => {
|
||||
let mut locked_data = self.app_data.lock();
|
||||
match self.gui_state.lock().selected_panel {
|
||||
@@ -224,7 +252,18 @@ impl InputHandler {
|
||||
MouseEventKind::ScrollUp => self.previous(),
|
||||
MouseEventKind::ScrollDown => self.next(),
|
||||
MouseEventKind::Down(MouseButton::Left) => {
|
||||
self.gui_state.lock().rect_insersects(Rect::new(
|
||||
let header_intersects = self.gui_state.lock().header_intersect(Rect::new(
|
||||
mouse_event.column,
|
||||
mouse_event.row,
|
||||
1,
|
||||
1,
|
||||
));
|
||||
|
||||
if let Some(header) = header_intersects {
|
||||
self.sort(header);
|
||||
}
|
||||
|
||||
self.gui_state.lock().panel_intersect(Rect::new(
|
||||
mouse_event.column,
|
||||
mouse_event.row,
|
||||
1,
|
||||
|
||||
+110
-76
@@ -14,6 +14,7 @@ use tui::{
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::app_data::{Header, SortedOrder};
|
||||
use crate::{
|
||||
app_data::{AppData, ByteStats, Columns, CpuStats, State, Stats},
|
||||
app_error::AppError,
|
||||
@@ -47,7 +48,7 @@ fn generate_block<'a>(
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
panel: SelectablePanel,
|
||||
) -> Block<'a> {
|
||||
gui_state.lock().insert_into_area_map(panel, area);
|
||||
gui_state.lock().insert_into_panel_map(panel, area);
|
||||
let mut block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded);
|
||||
@@ -145,7 +146,7 @@ pub fn draw_containers<B: Backend>(
|
||||
state_style,
|
||||
),
|
||||
Span::styled(
|
||||
format!("{}{:>width$}", MARGIN, i.status, width = widths.status.1),
|
||||
format!("{}{:>width$}", MARGIN, i.status, width = &widths.status.1),
|
||||
state_style,
|
||||
),
|
||||
Span::styled(
|
||||
@@ -153,12 +154,12 @@ pub fn draw_containers<B: Backend>(
|
||||
"{}{:>width$}",
|
||||
MARGIN,
|
||||
i.cpu_stats.back().unwrap_or(&CpuStats::new(0.0)),
|
||||
width = widths.cpu.1
|
||||
width = &widths.cpu.1
|
||||
),
|
||||
state_style,
|
||||
),
|
||||
Span::styled(
|
||||
format!("{}{:>width$}", MARGIN, mems, width = widths.mem.1),
|
||||
format!("{}{:>width$}", MARGIN, mems, width = &widths.mem.1),
|
||||
state_style,
|
||||
),
|
||||
Span::styled(
|
||||
@@ -166,7 +167,7 @@ pub fn draw_containers<B: Backend>(
|
||||
"{}{:>width$}",
|
||||
MARGIN,
|
||||
i.id.chars().take(8).collect::<String>(),
|
||||
width = widths.id.1
|
||||
width = &widths.id.1
|
||||
),
|
||||
blue,
|
||||
),
|
||||
@@ -190,7 +191,6 @@ pub fn draw_containers<B: Backend>(
|
||||
ListItem::new(lines)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if items.is_empty() {
|
||||
let debug_text = String::from("no containers running");
|
||||
let paragraph = Paragraph::new(debug_text)
|
||||
@@ -357,81 +357,104 @@ pub fn draw_heading_bar<B: Backend>(
|
||||
f: &mut Frame<'_, B>,
|
||||
has_containers: bool,
|
||||
loading_icon: String,
|
||||
info_visible: bool,
|
||||
sorted_by: Option<(Header, SortedOrder)>,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
) {
|
||||
let block = || Block::default().style(Style::default().bg(Color::Magenta).fg(Color::Black));
|
||||
let info_visible = gui_state.lock().show_help;
|
||||
|
||||
f.render_widget(block(), area);
|
||||
|
||||
let mut column_headings = format!(
|
||||
" {}{:>width$}",
|
||||
loading_icon,
|
||||
columns.state.0,
|
||||
width = columns.state.1
|
||||
);
|
||||
column_headings.push_str(
|
||||
format!(
|
||||
"{} {:>width$}",
|
||||
MARGIN,
|
||||
columns.status.0,
|
||||
width = columns.status.1
|
||||
// Generate a bloack for the header, if the header is currently being used to sort a column, then highlight it white
|
||||
let header_block = |x: &Header| {
|
||||
let mut color = Color::Black;
|
||||
let mut suffix = "";
|
||||
let mut suffix_margin = 0;
|
||||
if let Some((a, b)) = sorted_by.as_ref() {
|
||||
if x == a {
|
||||
match b {
|
||||
SortedOrder::Asc => suffix = " ⌃",
|
||||
SortedOrder::Desc => suffix = " ⌄",
|
||||
}
|
||||
suffix_margin = 2;
|
||||
color = Color::White
|
||||
};
|
||||
};
|
||||
(
|
||||
Block::default().style(Style::default().bg(Color::Magenta).fg(color)),
|
||||
suffix,
|
||||
suffix_margin,
|
||||
)
|
||||
.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.id.0, width = columns.id.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(),
|
||||
);
|
||||
};
|
||||
|
||||
// Generate block for the headers, state and status has a specific layout, others all equal
|
||||
// width is dependant on it that column is selected to sort - or not
|
||||
let gen_header = |header: &Header, width: usize| {
|
||||
let block = header_block(header);
|
||||
let text = match header {
|
||||
Header::State => format!(
|
||||
" {}{:>width$}{ic}",
|
||||
loading_icon,
|
||||
header,
|
||||
ic = block.1,
|
||||
width = width - block.2,
|
||||
),
|
||||
Header::Status => format!(
|
||||
"{} {:>width$}{ic}",
|
||||
MARGIN,
|
||||
header,
|
||||
ic = block.1,
|
||||
width = width - block.2
|
||||
),
|
||||
|
||||
_ => format!(
|
||||
"{}{:>width$}{ic}",
|
||||
MARGIN,
|
||||
header,
|
||||
ic = block.1,
|
||||
width = width - block.2
|
||||
),
|
||||
};
|
||||
let count = text.chars().count() as u16;
|
||||
let status = Paragraph::new(text)
|
||||
.block(block.0)
|
||||
.alignment(Alignment::Left);
|
||||
(status, count)
|
||||
};
|
||||
|
||||
// Meta data for iterate over to create blocks and correct widths
|
||||
let header_meta = [
|
||||
(Header::State, columns.state.1),
|
||||
(Header::Status, columns.status.1),
|
||||
(Header::Cpu, columns.cpu.1),
|
||||
(Header::Memory, columns.mem.1),
|
||||
(Header::Id, columns.id.1),
|
||||
(Header::Name, columns.name.1),
|
||||
(Header::Image, columns.image.1),
|
||||
(Header::Rx, columns.net_rx.1),
|
||||
(Header::Tx, columns.net_tx.1),
|
||||
];
|
||||
|
||||
let header_data = header_meta
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let header_block = gen_header(&i.0, i.1);
|
||||
(
|
||||
header_block.0,
|
||||
i.0.to_owned(),
|
||||
Constraint::Max(header_block.1),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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 column_width = area.width - info_width;
|
||||
let column_width = if column_width > 0 { column_width } 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)]
|
||||
};
|
||||
@@ -440,12 +463,21 @@ pub fn draw_heading_bar<B: Backend>(
|
||||
.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 container_splits = header_data.iter().map(|i| i.2).collect::<Vec<_>>();
|
||||
|
||||
let headers_section = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(container_splits.as_ref())
|
||||
.split(split_bar[0]);
|
||||
|
||||
// draw the actual header blocks
|
||||
for (index, (paragraph, header, _)) in header_data.into_iter().enumerate() {
|
||||
gui_state
|
||||
.lock()
|
||||
.insert_into_header_map(header, headers_section[index]);
|
||||
f.render_widget(paragraph, headers_section[index]);
|
||||
}
|
||||
}
|
||||
|
||||
let paragraph = Paragraph::new(info_text)
|
||||
@@ -479,6 +511,8 @@ pub fn draw_help_box<B: Backend>(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 ( 0 ) stop sort");
|
||||
help_text.push_str("\n ( 1 - 9 ) sort by header - or click header");
|
||||
help_text.push_str(
|
||||
"\n ( m ) to toggle mouse capture - if disabled, text on screen can be selected & copied",
|
||||
);
|
||||
|
||||
+30
-7
@@ -1,6 +1,8 @@
|
||||
use std::{collections::HashMap, fmt};
|
||||
use tui::layout::{Constraint, Rect};
|
||||
|
||||
use crate::app_data::Header;
|
||||
|
||||
#[derive(Debug, PartialEq, std::hash::Hash, std::cmp::Eq, Clone, Copy)]
|
||||
pub enum SelectablePanel {
|
||||
Containers,
|
||||
@@ -165,7 +167,8 @@ 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>,
|
||||
panel_map: HashMap<SelectablePanel, Rect>,
|
||||
heading_map: HashMap<Header, Rect>,
|
||||
loading_icon: Loading,
|
||||
// Should be a vec, each time loading add a new to the vec, and reset remove from vec
|
||||
// for for if is_loading just check if vec is empty or not
|
||||
@@ -179,7 +182,8 @@ impl GuiState {
|
||||
/// Generate a default gui_state
|
||||
pub fn default() -> Self {
|
||||
Self {
|
||||
area_map: HashMap::new(),
|
||||
panel_map: HashMap::new(),
|
||||
heading_map: HashMap::new(),
|
||||
loading_icon: Loading::One,
|
||||
selected_panel: SelectablePanel::Containers,
|
||||
show_help: false,
|
||||
@@ -190,13 +194,13 @@ impl GuiState {
|
||||
|
||||
/// 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();
|
||||
self.panel_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) {
|
||||
pub fn panel_intersect(&mut self, rect: Rect) {
|
||||
if let Some(data) = self
|
||||
.area_map
|
||||
.panel_map
|
||||
.iter()
|
||||
.filter(|i| i.1.intersects(rect))
|
||||
.collect::<Vec<_>>()
|
||||
@@ -206,9 +210,28 @@ impl GuiState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
|
||||
pub fn header_intersect(&mut self, rect: Rect) -> Option<Header> {
|
||||
self.heading_map
|
||||
.iter()
|
||||
.filter(|i| i.1.intersects(rect))
|
||||
.collect::<Vec<_>>()
|
||||
.get(0)
|
||||
.map(|data| data.0.to_owned())
|
||||
}
|
||||
|
||||
/// 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);
|
||||
/// Remove each time, as terminal may have been resized!
|
||||
pub fn insert_into_panel_map(&mut self, panel: SelectablePanel, area: Rect) {
|
||||
self.panel_map.remove(&panel);
|
||||
self.panel_map.insert(panel, area);
|
||||
}
|
||||
|
||||
/// Insert selectable gui panel into area map
|
||||
/// Remove each time, as terminal may have been resized!
|
||||
pub fn insert_into_header_map(&mut self, header: Header, area: Rect) {
|
||||
self.heading_map.remove(&header);
|
||||
self.heading_map.insert(header, area);
|
||||
}
|
||||
|
||||
/// Change to next selectable panel
|
||||
|
||||
+4
-1
@@ -154,6 +154,8 @@ fn ui<B: Backend>(
|
||||
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 sorted_by = app_data.lock().get_sorted();
|
||||
|
||||
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();
|
||||
@@ -213,7 +215,8 @@ fn ui<B: Backend>(
|
||||
f,
|
||||
has_containers,
|
||||
loading_icon,
|
||||
show_help,
|
||||
sorted_by,
|
||||
gui_state,
|
||||
);
|
||||
|
||||
// only draw charts if there are containers
|
||||
|
||||
Reference in New Issue
Block a user