From c190f0206cc55b8e45b8373f9be954e828c18b3b Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 14 Aug 2025 23:20:44 +0000 Subject: [PATCH 01/20] feat: horizontally scroll across log By default, use left and right arrow keys to horizontally scroll over the lines of logs, also has various refactors to reduced to size of the vec of logs sent to the ui renderer --- .gitignore | 1 + example_config/example.config.jsonc | 7 + example_config/example.config.toml | 3 + src/app_data/container_state.rs | 200 ++++++++++++--- src/app_data/mod.rs | 89 +++++-- src/config/config.toml | 4 + src/config/keymap_parser.rs | 18 ++ src/input_handler/mod.rs | 46 +++- src/ui/draw_blocks/help.rs | 228 ++++++++++-------- src/ui/draw_blocks/logs.rs | 2 +- src/ui/draw_blocks/mod.rs | 12 +- ...blocks__help__tests__draw_blocks_help.snap | 3 +- ...tests__draw_blocks_help_custom_colors.snap | 2 +- ...cks_help_custom_keymap_one_definition.snap | 96 ++++---- ...ks_help_custom_keymap_two_definitions.snap | 96 ++++---- ...w_blocks_help_one_and_two_definitions.snap | 96 ++++---- ...tests__draw_blocks_help_show_timezone.snap | 2 +- ...__draw_blocks_whole_layout_help_panel.snap | 20 +- src/ui/gui_state.rs | 12 + src/ui/mod.rs | 12 +- 20 files changed, 617 insertions(+), 332 deletions(-) diff --git a/.gitignore b/.gitignore index 5388e4b..ee5b384 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target /releases +/binaries # Used in the zigbuild for aarch64-apple-darwin .intentionally-empty-file.o \ No newline at end of file diff --git a/example_config/example.config.jsonc b/example_config/example.config.jsonc index 230e982..af38848 100644 --- a/example_config/example.config.jsonc +++ b/example_config/example.config.jsonc @@ -100,6 +100,13 @@ "up", "k" ], + // Horizontal scroll of the logs + "log_scroll_forward": [ + "right" + ], + "log_scroll_back": [ + "left" + ], // Select next panel "select_next_panel": [ "tab" diff --git a/example_config/example.config.toml b/example_config/example.config.toml index 3c71386..46ff71f 100644 --- a/example_config/example.config.toml +++ b/example_config/example.config.toml @@ -86,6 +86,9 @@ scroll_start = ["home"] scroll_up_many = ["pageup"] # scroll up a list by one item scroll_up_one = ["up", "k"] +# Horizontal scroll of the logs +log_scroll_forward = ["right"] +log_scroll_back = ["left"] # Select next panel select_next_panel = ["tab"] # Select previous panel diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index de27aac..97e9f42 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -7,10 +7,7 @@ use std::{ use bollard::service::Port; use jiff::{Timestamp, tz::TimeZone}; -use ratatui::{ - style::Color, - widgets::{ListItem, ListState}, -}; +use ratatui::{layout::Size, style::Color, text::Text, widgets::ListState}; use crate::config::AppColors; @@ -563,81 +560,166 @@ impl LogsTz { /// stateful list dependent on whether the timestamp is in the HashSet or not #[derive(Debug, Clone, PartialEq, Eq)] pub struct Logs { - logs: StatefulList>, + // should just be list of spans? + lines: StatefulList>, tz: HashSet, + // could probably be a u16 + offset: u16, + max_log_len: usize, + adjusted_max_width: usize, } impl Default for Logs { fn default() -> Self { - let mut logs = StatefulList::new(vec![]); - logs.end(); + let mut lines = StatefulList::new(vec![]); + lines.end(); Self { - logs, + lines, tz: HashSet::new(), + offset: 0, + adjusted_max_width: 0, + max_log_len: 0, } } } impl Logs { /// Only allow a new log line to be inserted if the log timestamp isn't in the tz HashSet - pub fn insert(&mut self, line: ListItem<'static>, tz: LogsTz) { + pub fn insert(&mut self, line: Text<'static>, tz: LogsTz) { if self.tz.insert(tz) { - self.logs.items.push(line); + self.max_log_len = self.max_log_len.max(line.width()); + self.lines.items.push(line); } } - /// Get the logs vec, but instead of cloning to whole vec, only clone items with x of the currently selected index + /// If scrolling horiztonally along the logs, display a counter of the position in the in the scroll, `x/y` + pub fn get_scroll_title(&self) -> Option { + if self.offset > 0 { + Some(format!(" {}/{} ", self.offset, self.adjusted_max_width)) + } else { + None + } + } + + /// Format a log lone. Only return screen width amount of chars + /// If offset set, remove `char_offset` number of chars from a Text + /// `text` *should* only be a single line, so just use the .first() method rather than trying to iterate + fn format_log_line(text: &Text<'static>, char_offset: usize, width: u16) -> Text<'static> { + let mut skipped = 0; + Text::from( + text.lines + .first() + .map(|line| { + ratatui::text::Line::from( + line.spans + .iter() + .filter_map(|span| { + if skipped >= char_offset { + return Some(ratatui::text::Span::styled( + span.content.chars().take(width.into()).collect::(), + span.style, + )); + } + let span_len = span.content.chars().count(); + if skipped + span_len <= char_offset { + skipped += span_len; + None + } else { + let start_index = char_offset - skipped; + skipped = char_offset; + let new_content = span + .content + .chars() + .skip(start_index) + .take(width.into()) + .collect::(); + Some(ratatui::text::Span::styled(new_content, span.style)) + } + }) + .collect::>(), + ) + }) + .into_iter() + .collect::>(), + ) + } + + /// Get the logs vec, but instead of cloning to whole vec, only clone items within x of the currently selected index, as ell as only the current screen widths number of chars /// Where x is the abs different of the index plus the panel height & a padding + /// Take into account the char offset, so that can scroll a line /// The rest can be just empty list items - pub fn to_vec(&self, height: usize, padding: usize) -> Vec> { - let current_index = self.logs.state.selected().unwrap_or_default(); - self.logs + pub fn get_visible_logs(&self, size: Size, padding: usize) -> Vec> { + let current_index = self.lines.state.selected().unwrap_or_default(); + let height_padding = usize::from(size.height) + padding; + let char_offset = if usize::from(self.offset) > self.max_log_len { + self.max_log_len + } else { + self.offset.into() + }; + + self.lines .items .iter() .enumerate() .map(|(index, item)| { - if current_index.abs_diff(index) <= height + padding { - item.clone() + if current_index.abs_diff(index) <= height_padding { + Self::format_log_line(item, char_offset, size.width) } else { - ListItem::from("") + Text::from("") } }) .collect() } + /// The rest of the methods are basically forwarding from the underlying StatefulList pub fn get_state_title(&self) -> String { - self.logs.get_state_title() + self.lines.get_state_title() + } + + /// Add a padding so one char will always be visilbe? + /// +6 is to account for borders & the selection triangle and a little bit of padding + pub fn forward(&mut self, width: u16) { + let offset = usize::from(self.offset); + self.adjusted_max_width = self.max_log_len.saturating_sub(width.into()) + 6; + if self.adjusted_max_width > 0 && offset < self.adjusted_max_width { + self.offset = self.offset.saturating_add(1); + } + } + + /// Reduce the char offset + pub const fn back(&mut self) { + self.offset = self.offset.saturating_sub(1); } pub fn next(&mut self) { - self.logs.next(); + self.lines.next(); } pub fn previous(&mut self) { - self.logs.previous(); + self.lines.previous(); } pub fn end(&mut self) { - self.logs.end(); + self.lines.end(); } pub fn start(&mut self) { - self.logs.start(); + self.lines.start(); } - // TODO remove this once zigbuild uses Rust v1.87.0 - #[cfg(target_os = "macos")] - #[allow(clippy::missing_const_for_fn)] - pub fn len(&self) -> usize { - self.logs.items.len() - } + // // TODO remove this once zigbuild uses Rust v1.87.0 + // #[cfg(target_os = "macos")] + // #[allow(clippy::missing_const_for_fn)] + // pub fn len(&self) -> usize { + // self.logs.items.len() + // } - #[cfg(not(target_os = "macos"))] + // #[cfg(not(target_os = "macos"))] pub const fn len(&self) -> usize { - self.logs.items.len() + self.lines.items.len() } pub const fn state(&mut self) -> &mut ListState { - &mut self.logs.state + &mut self.lines.state } } @@ -801,7 +883,10 @@ impl Columns { mod tests { use jiff::tz::TimeZone; - use ratatui::widgets::ListItem; + use ratatui::{ + layout::Size, + text::{Line, Text}, + }; use crate::{ app_data::{ContainerImage, Logs, LogsTz, RunningState}, @@ -941,21 +1026,21 @@ mod tests { let mut logs = Logs::default(); let line = log_sanitizer::remove_ansi(input); - logs.insert(ListItem::new(line.clone()), tz.clone()); - logs.insert(ListItem::new(line.clone()), tz.clone()); - logs.insert(ListItem::new(line), tz); + logs.insert(Text::from(line.clone()), tz.clone()); + logs.insert(Text::from(line.clone()), tz.clone()); + logs.insert(Text::from(line), tz); - assert_eq!(logs.logs.items.len(), 1); + assert_eq!(logs.lines.items.len(), 1); let input = "2023-01-15T19:13:30.783138328Z Lorem ipsum dolor sit amet"; let (tz, _) = LogsTz::splitter(input); let line = log_sanitizer::remove_ansi(input); - logs.insert(ListItem::new(line.clone()), tz.clone()); - logs.insert(ListItem::new(line.clone()), tz.clone()); - logs.insert(ListItem::new(line), tz); + logs.insert(Text::from(line.clone()), tz.clone()); + logs.insert(Text::from(line.clone()), tz.clone()); + logs.insert(Text::from(line), tz); - assert_eq!(logs.logs.items.len(), 2); + assert_eq!(logs.lines.items.len(), 2); } #[test] @@ -1008,4 +1093,39 @@ mod tests { let input = State::from(("oxker", &healthy)); assert_eq!(input, State::Unknown); } + + #[test] + /// Test the format_log_line methods, should ideally check colours are being correct kept as well + fn test_to_vec() { + let mut logs = Logs::default(); + + let input = "2023-01-14T19:13:30.783138328Z Hello world some long line".to_owned(); + let (tz, _) = LogsTz::splitter(&input); + logs.insert(Text::from(input), tz); + + let input = "2023-01-14T19:13:31.783138328Z Hello world some line".to_owned(); + let (tz, _) = LogsTz::splitter(&input); + logs.insert(Text::from(input), tz); + + let input = "2023-01-14T19:13:32.783138328Z Hello world".to_owned(); + let (tz, _) = LogsTz::splitter(&input); + logs.insert(Text::from(input), tz); + + logs.offset = 43; + let result = logs.get_visible_logs( + Size { + width: 14, + height: 10, + }, + 10, + ); + assert_eq!( + vec![ + Text::from(Line::from("some long line")), + Text::from(Line::from("some line")), + Text::from(Line::default()) + ], + result + ); + } } diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 3301476..a6e17af 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -1,7 +1,7 @@ use bollard::models::ContainerSummary; use core::fmt; use parking_lot::Mutex; -use ratatui::widgets::{ListItem, ListState}; +use ratatui::{layout::Size, text::Text, widgets::ListState}; use std::{ hash::Hash, sync::Arc, @@ -644,6 +644,28 @@ impl AppData { }) } + /// If scrolling horiztonally along the logs, display a counter of the position in the in the scroll, `x/y` + pub fn get_scroll_title(&self) -> Option { + self.get_selected_container() + .and_then(|i| i.logs.get_scroll_title()) + } + + /// Increase the logs offset, basically moving an invisible cursor back + pub fn log_back(&mut self) { + if let Some(i) = self.get_mut_selected_container() { + i.logs.back(); + self.redraw.update(); + } + } + + /// Increase the logs offset, basically moving an invisible cursor forward + pub fn log_forward(&mut self, width: u16) { + if let Some(i) = self.get_mut_selected_container() { + i.logs.forward(width); + self.redraw.update(); + } + } + /// select next selected log line pub fn log_next(&mut self) { if let Some(i) = self.get_mut_selected_container() { @@ -677,12 +699,12 @@ impl AppData { } /// Get mutable Vec of current containers logs - pub fn get_logs(&self, height: u16, padding: usize) -> Vec> { + pub fn get_logs(&self, size: Size, padding: usize) -> Vec> { self.containers .state .selected() .and_then(|i| self.containers.items.get(i)) - .map_or(vec![], |i| i.logs.to_vec(height.into(), padding)) + .map_or(vec![], |i| i.logs.get_visible_logs(size, padding)) } /// Get mutable Option of the currently selected container Logs state @@ -965,7 +987,7 @@ impl AppData { } else { log_sanitizer::remove_ansi(&i) }; - container.logs.insert(ListItem::new(lines), log_tz); + container.logs.insert(Text::from(lines), log_tz); } // Set the logs selected row for each container @@ -1945,14 +1967,19 @@ mod tests { let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); app_data.update_log_by_id(logs, &ids[0]); - // app_data.log_start(); let result = app_data.get_log_state(); assert!(result.is_some()); assert_eq!(result.as_ref().unwrap().selected(), Some(2)); assert_eq!(result.unwrap().offset(), 0); - let result = app_data.get_logs(4, 1); + let result = app_data.get_logs( + Size { + width: 20, + height: 4, + }, + 1, + ); assert_eq!(result.len(), 3); let result = app_data.get_log_title(); @@ -2340,44 +2367,68 @@ mod tests { app_data.update_log_by_id(logs, &ids[0]); - let result = app_data.get_logs(10, 10); + let result = app_data.get_logs( + Size { + width: 20, + height: 10, + }, + 10, + ); for (index, item) in result.iter().enumerate() { if index < 979 { - assert_eq!(item, &ListItem::new("")); + assert_eq!(item, &Text::from("")); } else { - assert_eq!(item, &ListItem::new(format!("{index}"))); + assert_eq!(item, &Text::from(format!("{index}"))); } } - let result = app_data.get_logs(100, 20); + let result = app_data.get_logs( + Size { + width: 20, + height: 100, + }, + 20, + ); for (index, item) in result.iter().enumerate() { if index < 879 { - assert_eq!(item, &ListItem::new("")); + assert_eq!(item, &Text::from("")); } else { - assert_eq!(item, &ListItem::new(format!("{index}"))); + assert_eq!(item, &Text::from(format!("{index}"))); } } app_data.log_start(); - let result = app_data.get_logs(10, 10); + + let result = app_data.get_logs( + Size { + width: 20, + height: 10, + }, + 10, + ); for (index, item) in result.iter().enumerate() { if index > 20 { - assert_eq!(item, &ListItem::new("")); + assert_eq!(item, &Text::from("")); } else { - assert_eq!(item, &ListItem::new(format!("{index}"))); + assert_eq!(item, &Text::from(format!("{index}"))); } } for _ in 0..=500 { app_data.log_next(); } - - let result = app_data.get_logs(10, 10); + let result = app_data.get_logs( + Size { + width: 20, + height: 10, + }, + 10, + ); for (index, item) in result.iter().enumerate() { if (481..=521).contains(&index) { - assert_eq!(item, &ListItem::new(format!("{index}"))); + assert_eq!(item, &Text::from(format!("{index}"))); } else { - assert_eq!(item, &ListItem::new("")); + assert_eq!(item, &Text::from("")); } } } diff --git a/src/config/config.toml b/src/config/config.toml index 457064a..37dbd13 100644 --- a/src/config/config.toml +++ b/src/config/config.toml @@ -77,6 +77,7 @@ save_logs = ["s"] scroll_down_many = ["pagedown"] # scroll down a list by one item scroll_down_one = ["down", "j"] + # scroll down to the end of a list scroll_end = ["end"] # scroll up to the start of a list @@ -85,6 +86,9 @@ scroll_start = ["home"] scroll_up_many = ["pageup"] # scroll up a list by one item scroll_up_one = ["up", "k"] +# Horizontal scroll of the logs +log_scroll_forward = ["right"] +log_scroll_back = ["left"] # Select next panel select_next_panel = ["tab"] # Select previous panel diff --git a/src/config/keymap_parser.rs b/src/config/keymap_parser.rs index 9847800..e66acef 100644 --- a/src/config/keymap_parser.rs +++ b/src/config/keymap_parser.rs @@ -42,6 +42,8 @@ optional_config_struct!( log_section_height_increase, log_section_height_decrease, log_section_toggle, + log_scroll_forward, + log_scroll_back, quit, save_logs, scroll_down_many, @@ -76,6 +78,8 @@ config_struct!( log_section_height_increase, log_section_height_decrease, log_section_toggle, + log_scroll_forward, + log_scroll_back, quit, save_logs, scroll_down_many, @@ -111,6 +115,8 @@ impl Keymap { log_section_height_decrease: (KeyCode::Char('-'), None), log_section_height_increase: (KeyCode::Char('='), None), log_section_toggle: (KeyCode::Char('\\'), None), + log_scroll_back: (KeyCode::Left, None), + log_scroll_forward: (KeyCode::Right, None), quit: (KeyCode::Char('q'), None), save_logs: (KeyCode::Char('s'), None), scroll_down_many: (KeyCode::PageDown, None), @@ -201,6 +207,12 @@ impl From> for Keymap { update_keymap(ck.scroll_start, &mut keymap.scroll_start, &mut clash); update_keymap(ck.scroll_up_many, &mut keymap.scroll_up_many, &mut clash); update_keymap(ck.scroll_up_one, &mut keymap.scroll_up_one, &mut clash); + update_keymap( + ck.log_scroll_forward, + &mut keymap.log_scroll_forward, + &mut clash, + ); + update_keymap(ck.log_scroll_back, &mut keymap.log_scroll_back, &mut clash); update_keymap( ck.select_next_panel, &mut keymap.select_next_panel, @@ -366,6 +378,8 @@ mod tests { exec: None, log_section_height_decrease: None, log_section_height_increase: None, + log_scroll_forward: None, + log_scroll_back: None, filter_mode: None, quit: None, save_logs: None, @@ -410,6 +424,8 @@ mod tests { filter_mode: gen_v(("i", "j")), log_section_height_decrease: gen_v(("-", "Z")), log_section_height_increase: gen_v(("=", "X")), + log_scroll_forward: gen_v(("right", "R")), + log_scroll_back: gen_v(("left", "L")), log_section_toggle: gen_v(("Y", "W")), quit: gen_v(("k", "l")), save_logs: gen_v(("m", "n")), @@ -444,6 +460,8 @@ mod tests { log_section_height_decrease: (KeyCode::Char('-'), Some(KeyCode::Char('Z'))), log_section_height_increase: (KeyCode::Char('='), Some(KeyCode::Char('X'))), log_section_toggle: (KeyCode::Char('Y'), Some(KeyCode::Char('W'))), + log_scroll_forward: (KeyCode::Right, Some(KeyCode::Char('R'))), + log_scroll_back: (KeyCode::Left, Some(KeyCode::Char('L'))), exec: (KeyCode::Char('g'), Some(KeyCode::Char('h'))), filter_mode: (KeyCode::Char('i'), Some(KeyCode::Char('j'))), quit: (KeyCode::Char('k'), Some(KeyCode::Char('l'))), diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 077d5c3..1d5c22b 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -286,6 +286,23 @@ impl InputHandler { } } + /// Advance the "cursor" along the logs + fn logs_forward(&self) { + let panel = self.gui_state.lock().get_selected_panel(); + if panel == SelectablePanel::Logs { + let width = self.gui_state.lock().get_screen_width(); + self.app_data.lock().log_forward(width); + } + } + + /// Retreat the "cursor" along the logs + fn logs_back(&self) { + let panel = self.gui_state.lock().get_selected_panel(); + if panel == SelectablePanel::Logs { + self.app_data.lock().log_back(); + } + } + /// Change the the "next" selectable panel /// If no containers, and on Commands panel, skip to next panel, as Commands panel isn't visible in this state fn next_panel_key(&self) { @@ -467,6 +484,7 @@ impl InputHandler { } /// Handle button presses in all other scenarios + #[allow(clippy::cognitive_complexity)] async fn handle_others(&mut self, key_code: KeyCode) { self.handle_sort(key_code); // shift key plus arrows @@ -537,28 +555,28 @@ impl InputHandler { _ if self.keymap.scroll_up_one.0 == key_code || self.keymap.scroll_up_one.1 == Some(key_code) => { - self.previous(); + self.scroll_up(); } _ if self.keymap.scroll_up_many.0 == key_code || self.keymap.scroll_up_many.1 == Some(key_code) => { for _ in 0..=6 { - self.previous(); + self.scroll_up(); } } _ if self.keymap.scroll_down_one.0 == key_code || self.keymap.scroll_down_one.1 == Some(key_code) => { - self.next(); + self.scroll_down(); } _ if self.keymap.scroll_down_many.0 == key_code || self.keymap.scroll_down_many.1 == Some(key_code) => { for _ in 0..=6 { - self.next(); + self.scroll_down(); } } @@ -569,6 +587,18 @@ impl InputHandler { self.docker_tx.send(DockerMessage::Update).await.ok(); } + _ if self.keymap.log_scroll_back.0 == key_code + || self.keymap.log_scroll_back.1 == Some(key_code) => + { + self.logs_back(); + } + + _ if self.keymap.log_scroll_forward.0 == key_code + || self.keymap.log_scroll_forward.1 == Some(key_code) => + { + self.logs_forward(); + } + KeyCode::Enter => self.enter_key().await, _ => (), } @@ -638,8 +668,8 @@ impl InputHandler { } } else { match mouse_event.kind { - MouseEventKind::ScrollUp => self.previous(), - MouseEventKind::ScrollDown => self.next(), + MouseEventKind::ScrollUp => self.scroll_up(), + MouseEventKind::ScrollDown => self.scroll_down(), MouseEventKind::Down(MouseButton::Left) => { let mouse_point = Rect::new(mouse_event.column, mouse_event.row, 1, 1); let header = self.gui_state.lock().get_intersect_header(mouse_point); @@ -659,7 +689,7 @@ impl InputHandler { } /// Change state to next, depending which panel is currently in focus - fn next(&self) { + fn scroll_down(&self) { let selected_panel = self.gui_state.lock().get_selected_panel(); match selected_panel { SelectablePanel::Containers => self.app_data.lock().containers_next(), @@ -669,7 +699,7 @@ impl InputHandler { } /// Change state to previous, depending which panel is currently in focus - fn previous(&self) { + fn scroll_up(&self) { let selected_panel = self.gui_state.lock().get_selected_panel(); match selected_panel { SelectablePanel::Containers => self.app_data.lock().containers_previous(), diff --git a/src/ui/draw_blocks/help.rs b/src/ui/draw_blocks/help.rs index a28cbbe..c861996 100644 --- a/src/ui/draw_blocks/help.rs +++ b/src/ui/draw_blocks/help.rs @@ -84,6 +84,7 @@ impl HelpInfo { } } + // todo ← → for log moving /// Generate the button information span + metadata #[allow(clippy::too_many_lines)] fn gen_keymap_info(colors: AppColors, zone: Option<&TimeZone>, show_timestamp: bool) -> Self { @@ -111,6 +112,11 @@ impl HelpInfo { button_item("Home End"), button_desc("change selected line"), ]), + Line::from(vec![ + space(), + button_item("← →"), + button_desc("horizontal scroll across logs"), + ]), Line::from(vec![ space(), button_item("enter"), @@ -268,6 +274,8 @@ impl HelpInfo { or_secondary(km.scroll_up_many, "scroll list by up many"), or_secondary(km.scroll_end, "scroll list to end"), or_secondary(km.scroll_start, "scroll list to start"), + or_secondary(km.log_scroll_forward, "horizontal scroll logs right"), + or_secondary(km.log_scroll_back, "horizontal scroll logs left"), Line::from(vec![ space(), button_item("enter"), @@ -436,6 +444,8 @@ mod tests { #[test] /// This will cause issues once the version has more than the current 5 chars (0.5.0) + /// println!("{} {} {} {} {}", row_index, result_cell_index, result_cell.symbol(), result_cell.bg, result_cell.fg); + /// TODO broken wihh the horizonal scrolls! fn test_draw_blocks_help() { let mut setup = test_setup(87, 35, true, true); let tz = setup.app_data.lock().config.timezone.clone(); @@ -463,30 +473,30 @@ mod tests { assert_eq!(result_cell.bg, Color::Reset); assert_eq!(result_cell.fg, Color::Reset); } - // border is black on magenta + // border is red on black (1 | 32, _) | (1..=31, 1 | 85) => { assert_eq!(result_cell.bg, Color::Magenta); assert_eq!(result_cell.fg, Color::Black); } - // oxker logo && description + // Buttons (2..=10, 2..=85) | (12, 19..=66) | (14, 2..=10 | 13..=27) | (15, 2..=10 | 13..=21 | 24..=40 | 43..=56) - | (16 | 23, 2..=12) - | (17..=20 | 22 | 25 | 27, 2..=8) - | (21, 2..=9 | 12..=18) - | (24 | 26, 2..=10) => { + | (16 | 25 | 27, 2..=10) + | (17 | 24, 2..=12) + | (18 | 19 | 20 | 21 | 23 | 26 | 28, 2..=8) + | (22, 2..=9 | 12..=18) => { assert_eq!(result_cell.bg, Color::Magenta); assert_eq!(result_cell.fg, Color::White); } - // The URL is white and underlined - (30, 25..=60) => { + // The URL is yellow and underlined + (31, 25..=60) => { assert_eq!(result_cell.bg, Color::Magenta); assert_eq!(result_cell.fg, Color::White); assert_eq!(result_cell.modifier, Modifier::UNDERLINED); } - // The rest is black on magenta + // The rest is red on black _ => { assert_eq!(result_cell.bg, Color::Magenta); assert_eq!(result_cell.fg, Color::Black); @@ -498,6 +508,8 @@ mod tests { #[test] /// Test that the help panel gets drawn with custom colors + /// This test is annoying + /// println!("{} {} {} {} {}", row_index, result_cell_index, result_cell.symbol(), result_cell.bg, result_cell.fg); fn test_draw_blocks_help_custom_colors() { let mut setup = test_setup(87, 35, true, true); let mut colors = AppColors::new(); @@ -535,20 +547,20 @@ mod tests { assert_eq!(result_cell.bg, Color::Black); assert_eq!(result_cell.fg, Color::Red); } - // oxker logo && description + // Buttons (2..=10, 2..=85) | (12, 19..=66) | (14, 2..=10 | 13..=27) | (15, 2..=10 | 13..=21 | 24..=40 | 43..=56) - | (16 | 23, 2..=12) - | (17..=20 | 22 | 25 | 27, 2..=8) - | (21, 2..=9 | 12..=18) - | (24 | 26, 2..=10) => { + | (16 | 25 | 27, 2..=10) + | (17 | 24, 2..=12) + | (18 | 19 | 20 | 21 | 23 | 26 | 28, 2..=8) + | (22, 2..=9 | 12..=18) => { assert_eq!(result_cell.bg, Color::Black); assert_eq!(result_cell.fg, Color::Yellow); } // The URL is yellow and underlined - (30, 25..=60) => { + (31, 25..=60) => { assert_eq!(result_cell.bg, Color::Black); assert_eq!(result_cell.fg, Color::Yellow); assert_eq!(result_cell.modifier, Modifier::UNDERLINED); @@ -566,39 +578,41 @@ mod tests { #[test] /// Help panel will show custom keymap if in use, with one definition for each entry fn test_draw_blocks_help_custom_keymap_one_definition() { - let mut setup = test_setup(98, 47, true, true); + let mut setup = test_setup(98, 49, true, true); let input = Keymap { clear: (KeyCode::Char('a'), None), + delete_confirm: (KeyCode::Char('b'), None), delete_deny: (KeyCode::Char('c'), None), - delete_confirm: (KeyCode::Char('e'), None), - exec: (KeyCode::Char('g'), None), - log_section_height_decrease: (KeyCode::Char('z'), None), - log_section_height_increase: (KeyCode::Char('x'), None), - log_section_toggle: (KeyCode::Char('W'), None), - filter_mode: (KeyCode::Char('i'), None), + exec: (KeyCode::Char('d'), None), + filter_mode: (KeyCode::Char('e'), None), + log_scroll_back: (KeyCode::Char('f'), None), + log_scroll_forward: (KeyCode::Char('g'), None), + log_section_height_decrease: (KeyCode::Char('h'), None), + log_section_height_increase: (KeyCode::Char('i'), None), + log_section_toggle: (KeyCode::Char('j'), None), quit: (KeyCode::Char('k'), None), - save_logs: (KeyCode::Char('m'), None), - scroll_down_many: (KeyCode::Char('o'), None), - scroll_down_one: (KeyCode::Char('q'), None), - scroll_end: (KeyCode::Char('s'), None), - scroll_start: (KeyCode::Char('u'), None), - scroll_up_many: (KeyCode::Char('w'), None), - scroll_up_one: (KeyCode::Char('y'), None), - select_next_panel: (KeyCode::Char('0'), None), - select_previous_panel: (KeyCode::Char('2'), None), - sort_by_name: (KeyCode::Char('4'), None), - sort_by_state: (KeyCode::Char('6'), None), - sort_by_status: (KeyCode::Char('8'), None), - sort_by_cpu: (KeyCode::F(1), None), - sort_by_memory: (KeyCode::Char('#'), None), - sort_by_id: (KeyCode::Char('/'), None), - sort_by_image: (KeyCode::Char(','), None), - sort_by_rx: (KeyCode::Char('.'), None), - sort_by_tx: (KeyCode::Insert, None), - sort_reset: (KeyCode::Up, None), - toggle_help: (KeyCode::Home, None), - toggle_mouse_capture: (KeyCode::PageDown, None), + save_logs: (KeyCode::Char('l'), None), + scroll_down_many: (KeyCode::Char('m'), None), + scroll_down_one: (KeyCode::Char('n'), None), + scroll_end: (KeyCode::Char('o'), None), + scroll_start: (KeyCode::Char('p'), None), + scroll_up_many: (KeyCode::Char('q'), None), + scroll_up_one: (KeyCode::Char('r'), None), + select_next_panel: (KeyCode::Char('s'), None), + select_previous_panel: (KeyCode::Char('t'), None), + sort_by_cpu: (KeyCode::Char('u'), None), + sort_by_id: (KeyCode::Char('v'), None), + sort_by_image: (KeyCode::Char('w'), None), + sort_by_memory: (KeyCode::Char('x'), None), + sort_by_name: (KeyCode::Char('y'), None), + sort_by_rx: (KeyCode::Char('z'), None), + sort_by_state: (KeyCode::Char('0'), None), + sort_by_status: (KeyCode::Char('1'), None), + sort_by_tx: (KeyCode::Char('2'), None), + sort_reset: (KeyCode::Char('3'), None), + toggle_help: (KeyCode::Char('4'), None), + toggle_mouse_capture: (KeyCode::Char('5'), None), }; setup @@ -614,39 +628,41 @@ mod tests { #[test] /// Help panel will show custom keymap if in use, with two definition for each entry fn test_draw_blocks_help_custom_keymap_two_definitions() { - let mut setup = test_setup(110, 47, true, true); + let mut setup = test_setup(110, 49, true, true); let keymap = Keymap { - clear: (KeyCode::Char('a'), Some(KeyCode::Char('b'))), - delete_deny: (KeyCode::Char('c'), Some(KeyCode::Char('d'))), - delete_confirm: (KeyCode::Char('e'), Some(KeyCode::Char('f'))), - exec: (KeyCode::Char('g'), Some(KeyCode::Char('h'))), - log_section_height_decrease: (KeyCode::Char('A'), Some(KeyCode::Char('Z'))), - log_section_height_increase: (KeyCode::Char('B'), Some(KeyCode::Char('X'))), - log_section_toggle: (KeyCode::Char('C'), Some(KeyCode::Char('W'))), - filter_mode: (KeyCode::Char('i'), Some(KeyCode::Char('j'))), - quit: (KeyCode::Char('k'), Some(KeyCode::Char('l'))), - save_logs: (KeyCode::Char('m'), Some(KeyCode::Char('n'))), - scroll_down_many: (KeyCode::Char('o'), Some(KeyCode::Char('p'))), - scroll_down_one: (KeyCode::Char('q'), Some(KeyCode::Char('r'))), - scroll_end: (KeyCode::Char('s'), Some(KeyCode::Char('t'))), - scroll_start: (KeyCode::Char('u'), Some(KeyCode::Char('v'))), - scroll_up_many: (KeyCode::Char('w'), Some(KeyCode::Char('x'))), - scroll_up_one: (KeyCode::Char('y'), Some(KeyCode::Char('z'))), - select_next_panel: (KeyCode::Char('0'), Some(KeyCode::Char('1'))), - select_previous_panel: (KeyCode::Char('2'), Some(KeyCode::Char('3'))), - sort_by_name: (KeyCode::Char('4'), Some(KeyCode::Char('5'))), - sort_by_state: (KeyCode::Char('6'), Some(KeyCode::Char('7'))), - sort_by_status: (KeyCode::Char('8'), Some(KeyCode::Char('9'))), - sort_by_cpu: (KeyCode::F(1), Some(KeyCode::F(12))), - sort_by_memory: (KeyCode::Char('#'), Some(KeyCode::Char('-'))), - sort_by_id: (KeyCode::Char('/'), Some(KeyCode::Char('='))), - sort_by_image: (KeyCode::Char(','), Some(KeyCode::Char('\\'))), - sort_by_rx: (KeyCode::Char('.'), Some(KeyCode::Char(']'))), - sort_by_tx: (KeyCode::Insert, Some(KeyCode::BackTab)), - sort_reset: (KeyCode::Up, Some(KeyCode::Down)), - toggle_help: (KeyCode::Home, Some(KeyCode::End)), - toggle_mouse_capture: (KeyCode::PageDown, Some(KeyCode::PageUp)), + clear: (KeyCode::Char('a'), Some(KeyCode::Char('A'))), + delete_confirm: (KeyCode::Char('b'), Some(KeyCode::Char('B'))), + delete_deny: (KeyCode::Char('c'), Some(KeyCode::Char('C'))), + exec: (KeyCode::Char('d'), Some(KeyCode::Char('D'))), + filter_mode: (KeyCode::Char('e'), Some(KeyCode::Char('E'))), + log_scroll_back: (KeyCode::Char('f'), Some(KeyCode::Char('F'))), + log_scroll_forward: (KeyCode::Char('g'), Some(KeyCode::Char('G'))), + log_section_height_decrease: (KeyCode::Char('h'), Some(KeyCode::Char('H'))), + log_section_height_increase: (KeyCode::Char('i'), Some(KeyCode::Char('I'))), + log_section_toggle: (KeyCode::Char('j'), Some(KeyCode::Char('J'))), + quit: (KeyCode::Char('k'), Some(KeyCode::Char('K'))), + save_logs: (KeyCode::Char('l'), Some(KeyCode::Char('L'))), + scroll_down_many: (KeyCode::Char('m'), Some(KeyCode::Char('M'))), + scroll_down_one: (KeyCode::Char('n'), Some(KeyCode::Char('N'))), + scroll_end: (KeyCode::Char('o'), Some(KeyCode::Char('O'))), + scroll_start: (KeyCode::Char('p'), Some(KeyCode::Char('P'))), + scroll_up_many: (KeyCode::Char('q'), Some(KeyCode::Char('Q'))), + scroll_up_one: (KeyCode::Char('r'), Some(KeyCode::Char('R'))), + select_next_panel: (KeyCode::Char('s'), Some(KeyCode::Char('S'))), + select_previous_panel: (KeyCode::Char('t'), Some(KeyCode::Char('T'))), + sort_by_cpu: (KeyCode::Char('u'), Some(KeyCode::Char('U'))), + sort_by_id: (KeyCode::Char('v'), Some(KeyCode::Char('V'))), + sort_by_image: (KeyCode::Char('w'), Some(KeyCode::Char('W'))), + sort_by_memory: (KeyCode::Char('x'), Some(KeyCode::Char('X'))), + sort_by_name: (KeyCode::Char('y'), Some(KeyCode::Char('Y'))), + sort_by_rx: (KeyCode::Char('z'), Some(KeyCode::Char('Z'))), + sort_by_state: (KeyCode::Char('0'), Some(KeyCode::Char('9'))), + sort_by_status: (KeyCode::Char('1'), Some(KeyCode::Char('8'))), + sort_by_tx: (KeyCode::Char('2'), Some(KeyCode::Char('7'))), + sort_reset: (KeyCode::Char('3'), Some(KeyCode::Char('6'))), + toggle_help: (KeyCode::Char('4'), Some(KeyCode::Char('5'))), + toggle_mouse_capture: (KeyCode::Char('5'), Some(KeyCode::PageDown)), }; setup @@ -662,39 +678,41 @@ mod tests { #[test] /// Help panel will show custom keymap if in use, with either one or two definition for each entry fn test_draw_blocks_help_one_and_two_definitions() { - let mut setup = test_setup(110, 47, true, true); + let mut setup = test_setup(110, 49, true, true); let keymap = Keymap { - clear: (KeyCode::Char('a'), Some(KeyCode::Char('b'))), - delete_deny: (KeyCode::Char('c'), None), - delete_confirm: (KeyCode::Char('e'), Some(KeyCode::Char('f'))), - exec: (KeyCode::Char('g'), None), - filter_mode: (KeyCode::Char('i'), Some(KeyCode::Char('j'))), - log_section_height_decrease: (KeyCode::Char('A'), Some(KeyCode::Char('Z'))), - log_section_height_increase: (KeyCode::Char('B'), Some(KeyCode::Char('X'))), - log_section_toggle: (KeyCode::Char('C'), Some(KeyCode::Char('W'))), - quit: (KeyCode::Char('k'), None), - save_logs: (KeyCode::Char('m'), Some(KeyCode::Char('n'))), - scroll_down_many: (KeyCode::Char('o'), None), - scroll_down_one: (KeyCode::Char('q'), Some(KeyCode::Char('r'))), - scroll_end: (KeyCode::Char('s'), None), - scroll_start: (KeyCode::Char('u'), Some(KeyCode::Char('v'))), - scroll_up_many: (KeyCode::Char('w'), None), - scroll_up_one: (KeyCode::Char('y'), Some(KeyCode::Char('z'))), - select_next_panel: (KeyCode::Char('0'), None), - select_previous_panel: (KeyCode::Char('2'), Some(KeyCode::Char('3'))), - sort_by_name: (KeyCode::Char('4'), None), - sort_by_state: (KeyCode::Char('6'), Some(KeyCode::Char('7'))), - sort_by_status: (KeyCode::Char('8'), None), - sort_by_cpu: (KeyCode::F(1), Some(KeyCode::F(12))), - sort_by_memory: (KeyCode::Char('#'), None), - sort_by_id: (KeyCode::Char('/'), Some(KeyCode::Char('='))), - sort_by_image: (KeyCode::Char(','), None), - sort_by_rx: (KeyCode::Char('.'), Some(KeyCode::Char(']'))), - sort_by_tx: (KeyCode::Insert, None), - sort_reset: (KeyCode::Up, Some(KeyCode::Down)), - toggle_help: (KeyCode::Home, None), - toggle_mouse_capture: (KeyCode::PageDown, Some(KeyCode::PageUp)), + clear: (KeyCode::Char('a'), Some(KeyCode::Char('A'))), + delete_confirm: (KeyCode::Char('b'), None), + delete_deny: (KeyCode::Char('c'), Some(KeyCode::Char('C'))), + exec: (KeyCode::Char('d'), None), + filter_mode: (KeyCode::Char('e'), Some(KeyCode::Char('E'))), + log_scroll_back: (KeyCode::Char('f'), None), + log_scroll_forward: (KeyCode::Char('g'), Some(KeyCode::Char('G'))), + log_section_height_decrease: (KeyCode::Char('h'), None), + log_section_height_increase: (KeyCode::Char('i'), Some(KeyCode::Char('I'))), + log_section_toggle: (KeyCode::Char('j'), None), + quit: (KeyCode::Char('k'), Some(KeyCode::Char('K'))), + save_logs: (KeyCode::Char('l'), None), + scroll_down_many: (KeyCode::Char('m'), Some(KeyCode::Char('M'))), + scroll_down_one: (KeyCode::Char('n'), None), + scroll_end: (KeyCode::Char('o'), Some(KeyCode::Char('O'))), + scroll_start: (KeyCode::Char('p'), None), + scroll_up_many: (KeyCode::Char('q'), Some(KeyCode::Char('Q'))), + scroll_up_one: (KeyCode::Char('r'), None), + select_next_panel: (KeyCode::Char('s'), Some(KeyCode::Char('S'))), + select_previous_panel: (KeyCode::Char('t'), None), + sort_by_cpu: (KeyCode::Char('u'), Some(KeyCode::Char('U'))), + sort_by_id: (KeyCode::Char('v'), None), + sort_by_image: (KeyCode::Char('w'), Some(KeyCode::Char('W'))), + sort_by_memory: (KeyCode::Char('x'), None), + sort_by_name: (KeyCode::Char('y'), Some(KeyCode::Char('Y'))), + sort_by_rx: (KeyCode::Char('z'), None), + sort_by_state: (KeyCode::Char('0'), Some(KeyCode::Char('9'))), + sort_by_status: (KeyCode::Char('1'), None), + sort_by_tx: (KeyCode::Char('2'), Some(KeyCode::Char('7'))), + sort_reset: (KeyCode::Char('3'), None), + toggle_help: (KeyCode::Char('4'), Some(KeyCode::Char('5'))), + toggle_mouse_capture: (KeyCode::Char('5'), None), }; let tz = setup.app_data.lock().config.timezone.clone(); diff --git a/src/ui/draw_blocks/logs.rs b/src/ui/draw_blocks/logs.rs index 6381e5b..72a2bcf 100644 --- a/src/ui/draw_blocks/logs.rs +++ b/src/ui/draw_blocks/logs.rs @@ -40,7 +40,7 @@ pub fn draw( f.render_widget(paragraph, area); } else { let padding = usize::from(area.height / 5); - let logs = app_data.lock().get_logs(area.height, padding); + let logs = app_data.lock().get_logs(area.as_size(), padding); if logs.is_empty() { let mut paragraph = Paragraph::new("no logs found") .block(block) diff --git a/src/ui/draw_blocks/mod.rs b/src/ui/draw_blocks/mod.rs index ec06c61..f97e753 100644 --- a/src/ui/draw_blocks/mod.rs +++ b/src/ui/draw_blocks/mod.rs @@ -72,7 +72,6 @@ pub fn max_line_width(text: &str) -> usize { .max() .unwrap_or_default() } - /// Generate block, add a border if is the selected panel, /// add custom title based on state of each panel fn generate_block<'a>( @@ -101,7 +100,15 @@ fn generate_block<'a>( let mut block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .title(title); + .title(ratatui::text::Line::from(title).left_aligned()); + + if panel == SelectablePanel::Logs { + if let Some(x) = fd.scroll_title.as_ref() { + block = block + .title_bottom(x.to_owned()) + .title_alignment(ratatui::layout::Alignment::Right); + } + } if !fd.status.contains(&Status::Filter) { if fd.selected_panel == panel { block = block.border_style(Style::default().fg(colors.borders.selected)); @@ -178,6 +185,7 @@ pub mod tests { loading_icon: gui_data.get_loading().to_string(), log_height: gui_data.get_log_height(), log_title: app_data.get_log_title(), + scroll_title: app_data.get_scroll_title(), port_max_lens: app_data.get_longest_port(), ports: app_data.get_selected_ports(), selected_panel: gui_data.get_selected_panel(), diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help.snap index 747eec0..8b03bdd 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help.snap @@ -1,6 +1,5 @@ --- source: src/ui/draw_blocks/help.rs -assertion_line: 456 expression: setup.terminal.backend() --- " " @@ -19,6 +18,7 @@ expression: setup.terminal.backend() " │ │ " " │ ( tab ) or ( shift+tab ) change panels │ " " │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) change selected line │ " +" │ ( ← → ) horizontal scroll across logs │ " " │ ( enter ) send docker container command │ " " │ ( e ) exec into a container │ " " │ ( h ) toggle this help information - or click heading │ " @@ -35,6 +35,5 @@ expression: setup.terminal.backend() " │ currently an early work in progress, all and any input appreciated │ " " │ https://github.com/mrjackwills/oxker │ " " │ │ " -" │ │ " " ╰───────────────────────────────────────────────────────────────────────────────────╯ " " " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_colors.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_colors.snap index a0f9ea1..8b03bdd 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_colors.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_colors.snap @@ -18,6 +18,7 @@ expression: setup.terminal.backend() " │ │ " " │ ( tab ) or ( shift+tab ) change panels │ " " │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) change selected line │ " +" │ ( ← → ) horizontal scroll across logs │ " " │ ( enter ) send docker container command │ " " │ ( e ) exec into a container │ " " │ ( h ) toggle this help information - or click heading │ " @@ -34,6 +35,5 @@ expression: setup.terminal.backend() " │ currently an early work in progress, all and any input appreciated │ " " │ https://github.com/mrjackwills/oxker │ " " │ │ " -" │ │ " " ╰───────────────────────────────────────────────────────────────────────────────────╯ " " " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_one_definition.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_one_definition.snap index 8cebda0..fe00d6a 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_one_definition.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_one_definition.snap @@ -2,50 +2,52 @@ source: src/ui/draw_blocks/help.rs expression: setup.terminal.backend() --- -" ╭ 0.00.000 ──────────────────────────────────────────────────────────────────────────────────╮ " -" │ │ " -" │ 88 │ " -" │ 88 │ " -" │ 88 │ " -" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ " -" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ " -" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ " -" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ " -" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ " -" │ │ " -" │ A simple tui to view & control docker containers │ " -" │ │ " -" │ ( 0 ) select next panel │ " -" │ ( 2 ) select previous panel │ " -" │ ( q ) scroll list down by one │ " -" │ ( y ) scroll list up by one │ " -" │ ( o ) scroll list down by many │ " -" │ ( w ) scroll list by up many │ " -" │ ( s ) scroll list to end │ " -" │ ( u ) scroll list to start │ " -" │ ( enter ) send docker container command │ " -" │ ( g ) exec into a container │ " -" │ ( Home ) toggle this help information - or click heading │ " -" │ ( m ) save logs to file │ " -" │ ( Page Down ) toggle mouse capture - if disabled, text on screen can be selected & copied │ " -" │ ( i ) enter filter mode │ " -" │ ( Up ) reset container sorting │ " -" │ ( 4 ) sort containers by name │ " -" │ ( 6 ) sort containers by state │ " -" │ ( 8 ) sort containers by status │ " -" │ ( F1 ) sort containers by cpu │ " -" │ ( # ) sort containers by memory │ " -" │ ( / ) sort containers by id │ " -" │ ( , ) sort containers by image │ " -" │ ( . ) sort containers by rx │ " -" │ ( Insert ) sort containers by tx │ " -" │ ( z ) decrease log section height │ " -" │ ( x ) increase log section height │ " -" │ ( W ) toggle log section visibility │ " -" │ ( a ) close dialog │ " -" │ ( k ) quit at any time │ " -" │ │ " -" │ currently an early work in progress, all and any input appreciated │ " -" │ https://github.com/mrjackwills/oxker │ " -" │ │ " -" ╰────────────────────────────────────────────────────────────────────────────────────────────╯ " +" ╭ 0.00.000 ──────────────────────────────────────────────────────────────────────────╮ " +" │ │ " +" │ 88 │ " +" │ 88 │ " +" │ 88 │ " +" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ " +" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ " +" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ " +" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ " +" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ " +" │ │ " +" │ A simple tui to view & control docker containers │ " +" │ │ " +" │ ( s ) select next panel │ " +" │ ( t ) select previous panel │ " +" │ ( n ) scroll list down by one │ " +" │ ( r ) scroll list up by one │ " +" │ ( m ) scroll list down by many │ " +" │ ( q ) scroll list by up many │ " +" │ ( o ) scroll list to end │ " +" │ ( p ) scroll list to start │ " +" │ ( g ) horizontal scroll logs right │ " +" │ ( f ) horizontal scroll logs left │ " +" │ ( enter ) send docker container command │ " +" │ ( d ) exec into a container │ " +" │ ( 4 ) toggle this help information - or click heading │ " +" │ ( l ) save logs to file │ " +" │ ( 5 ) toggle mouse capture - if disabled, text on screen can be selected & copied │ " +" │ ( e ) enter filter mode │ " +" │ ( 3 ) reset container sorting │ " +" │ ( y ) sort containers by name │ " +" │ ( 0 ) sort containers by state │ " +" │ ( 1 ) sort containers by status │ " +" │ ( u ) sort containers by cpu │ " +" │ ( x ) sort containers by memory │ " +" │ ( v ) sort containers by id │ " +" │ ( w ) sort containers by image │ " +" │ ( z ) sort containers by rx │ " +" │ ( 2 ) sort containers by tx │ " +" │ ( h ) decrease log section height │ " +" │ ( i ) increase log section height │ " +" │ ( j ) toggle log section visibility │ " +" │ ( a ) close dialog │ " +" │ ( k ) quit at any time │ " +" │ │ " +" │ currently an early work in progress, all and any input appreciated │ " +" │ https://github.com/mrjackwills/oxker │ " +" │ │ " +" ╰────────────────────────────────────────────────────────────────────────────────────╯ " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_two_definitions.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_two_definitions.snap index 818fc99..6ae3453 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_two_definitions.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_two_definitions.snap @@ -2,50 +2,52 @@ source: src/ui/draw_blocks/help.rs expression: setup.terminal.backend() --- -" ╭ 0.00.000 ────────────────────────────────────────────────────────────────────────────────────────────────╮ " -" │ │ " -" │ 88 │ " -" │ 88 │ " -" │ 88 │ " -" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ " -" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ " -" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ " -" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ " -" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ " -" │ │ " -" │ A simple tui to view & control docker containers │ " -" │ │ " -" │ ( 0 ) or ( 1 ) select next panel │ " -" │ ( 2 ) or ( 3 ) select previous panel │ " -" │ ( q ) or ( r ) scroll list down by one │ " -" │ ( y ) or ( z ) scroll list up by one │ " -" │ ( o ) or ( p ) scroll list down by many │ " -" │ ( w ) or ( x ) scroll list by up many │ " -" │ ( s ) or ( t ) scroll list to end │ " -" │ ( u ) or ( v ) scroll list to start │ " -" │ ( enter ) send docker container command │ " -" │ ( g ) or ( h ) exec into a container │ " -" │ ( Home ) or ( End ) toggle this help information - or click heading │ " -" │ ( m ) or ( n ) save logs to file │ " -" │ ( Page Down ) or ( Page Up ) toggle mouse capture - if disabled, text on screen can be selected & copied │ " -" │ ( i ) or ( j ) enter filter mode │ " -" │ ( Up ) or ( Down ) reset container sorting │ " -" │ ( 4 ) or ( 5 ) sort containers by name │ " -" │ ( 6 ) or ( 7 ) sort containers by state │ " -" │ ( 8 ) or ( 9 ) sort containers by status │ " -" │ ( F1 ) or ( F12 ) sort containers by cpu │ " -" │ ( # ) or ( - ) sort containers by memory │ " -" │ ( / ) or ( = ) sort containers by id │ " -" │ ( , ) or ( \ ) sort containers by image │ " -" │ ( . ) or ( ] ) sort containers by rx │ " -" │ ( Insert ) or ( Back Tab ) sort containers by tx │ " -" │ ( A ) or ( Z ) decrease log section height │ " -" │ ( B ) or ( X ) increase log section height │ " -" │ ( C ) or ( W ) toggle log section visibility │ " -" │ ( a ) or ( b ) close dialog │ " -" │ ( k ) or ( l ) quit at any time │ " -" │ │ " -" │ currently an early work in progress, all and any input appreciated │ " -" │ https://github.com/mrjackwills/oxker │ " -" │ │ " -" ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ " +" ╭ 0.00.000 ──────────────────────────────────────────────────────────────────────────────────────────╮ " +" │ │ " +" │ 88 │ " +" │ 88 │ " +" │ 88 │ " +" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ " +" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ " +" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ " +" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ " +" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ " +" │ │ " +" │ A simple tui to view & control docker containers │ " +" │ │ " +" │ ( s ) or ( S ) select next panel │ " +" │ ( t ) or ( T ) select previous panel │ " +" │ ( n ) or ( N ) scroll list down by one │ " +" │ ( r ) or ( R ) scroll list up by one │ " +" │ ( m ) or ( M ) scroll list down by many │ " +" │ ( q ) or ( Q ) scroll list by up many │ " +" │ ( o ) or ( O ) scroll list to end │ " +" │ ( p ) or ( P ) scroll list to start │ " +" │ ( g ) or ( G ) horizontal scroll logs right │ " +" │ ( f ) or ( F ) horizontal scroll logs left │ " +" │ ( enter ) send docker container command │ " +" │ ( d ) or ( D ) exec into a container │ " +" │ ( 4 ) or ( 5 ) toggle this help information - or click heading │ " +" │ ( l ) or ( L ) save logs to file │ " +" │ ( 5 ) or ( Page Down ) toggle mouse capture - if disabled, text on screen can be selected & copied │ " +" │ ( e ) or ( E ) enter filter mode │ " +" │ ( 3 ) or ( 6 ) reset container sorting │ " +" │ ( y ) or ( Y ) sort containers by name │ " +" │ ( 0 ) or ( 9 ) sort containers by state │ " +" │ ( 1 ) or ( 8 ) sort containers by status │ " +" │ ( u ) or ( U ) sort containers by cpu │ " +" │ ( x ) or ( X ) sort containers by memory │ " +" │ ( v ) or ( V ) sort containers by id │ " +" │ ( w ) or ( W ) sort containers by image │ " +" │ ( z ) or ( Z ) sort containers by rx │ " +" │ ( 2 ) or ( 7 ) sort containers by tx │ " +" │ ( h ) or ( H ) decrease log section height │ " +" │ ( i ) or ( I ) increase log section height │ " +" │ ( j ) or ( J ) toggle log section visibility │ " +" │ ( a ) or ( A ) close dialog │ " +" │ ( k ) or ( K ) quit at any time │ " +" │ │ " +" │ currently an early work in progress, all and any input appreciated │ " +" │ https://github.com/mrjackwills/oxker │ " +" │ │ " +" ╰────────────────────────────────────────────────────────────────────────────────────────────────────╯ " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_one_and_two_definitions.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_one_and_two_definitions.snap index 1778328..1b760e1 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_one_and_two_definitions.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_one_and_two_definitions.snap @@ -2,50 +2,52 @@ source: src/ui/draw_blocks/help.rs expression: setup.terminal.backend() --- -" ╭ 0.00.000 ────────────────────────────────────────────────────────────────────────────────────────────────╮ " -" │ │ " -" │ 88 │ " -" │ 88 │ " -" │ 88 │ " -" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ " -" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ " -" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ " -" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ " -" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ " -" │ │ " -" │ A simple tui to view & control docker containers │ " -" │ │ " -" │ ( 0 ) select next panel │ " -" │ ( 2 ) or ( 3 ) select previous panel │ " -" │ ( q ) or ( r ) scroll list down by one │ " -" │ ( y ) or ( z ) scroll list up by one │ " -" │ ( o ) scroll list down by many │ " -" │ ( w ) scroll list by up many │ " -" │ ( s ) scroll list to end │ " -" │ ( u ) or ( v ) scroll list to start │ " -" │ ( enter ) send docker container command │ " -" │ ( g ) exec into a container │ " -" │ ( Home ) toggle this help information - or click heading │ " -" │ ( m ) or ( n ) save logs to file │ " -" │ ( Page Down ) or ( Page Up ) toggle mouse capture - if disabled, text on screen can be selected & copied │ " -" │ ( i ) or ( j ) enter filter mode │ " -" │ ( Up ) or ( Down ) reset container sorting │ " -" │ ( 4 ) sort containers by name │ " -" │ ( 6 ) or ( 7 ) sort containers by state │ " -" │ ( 8 ) sort containers by status │ " -" │ ( F1 ) or ( F12 ) sort containers by cpu │ " -" │ ( # ) sort containers by memory │ " -" │ ( / ) or ( = ) sort containers by id │ " -" │ ( , ) sort containers by image │ " -" │ ( . ) or ( ] ) sort containers by rx │ " -" │ ( Insert ) sort containers by tx │ " -" │ ( A ) or ( Z ) decrease log section height │ " -" │ ( B ) or ( X ) increase log section height │ " -" │ ( C ) or ( W ) toggle log section visibility │ " -" │ ( a ) or ( b ) close dialog │ " -" │ ( k ) quit at any time │ " -" │ │ " -" │ currently an early work in progress, all and any input appreciated │ " -" │ https://github.com/mrjackwills/oxker │ " -" │ │ " -" ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ " +" ╭ 0.00.000 ──────────────────────────────────────────────────────────────────────────╮ " +" │ │ " +" │ 88 │ " +" │ 88 │ " +" │ 88 │ " +" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ " +" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ " +" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ " +" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ " +" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ " +" │ │ " +" │ A simple tui to view & control docker containers │ " +" │ │ " +" │ ( s ) or ( S ) select next panel │ " +" │ ( t ) select previous panel │ " +" │ ( n ) scroll list down by one │ " +" │ ( r ) scroll list up by one │ " +" │ ( m ) or ( M ) scroll list down by many │ " +" │ ( q ) or ( Q ) scroll list by up many │ " +" │ ( o ) or ( O ) scroll list to end │ " +" │ ( p ) scroll list to start │ " +" │ ( g ) or ( G ) horizontal scroll logs right │ " +" │ ( f ) horizontal scroll logs left │ " +" │ ( enter ) send docker container command │ " +" │ ( d ) exec into a container │ " +" │ ( 4 ) or ( 5 ) toggle this help information - or click heading │ " +" │ ( l ) save logs to file │ " +" │ ( 5 ) toggle mouse capture - if disabled, text on screen can be selected & copied │ " +" │ ( e ) or ( E ) enter filter mode │ " +" │ ( 3 ) reset container sorting │ " +" │ ( y ) or ( Y ) sort containers by name │ " +" │ ( 0 ) or ( 9 ) sort containers by state │ " +" │ ( 1 ) sort containers by status │ " +" │ ( u ) or ( U ) sort containers by cpu │ " +" │ ( x ) sort containers by memory │ " +" │ ( v ) sort containers by id │ " +" │ ( w ) or ( W ) sort containers by image │ " +" │ ( z ) sort containers by rx │ " +" │ ( 2 ) or ( 7 ) sort containers by tx │ " +" │ ( h ) decrease log section height │ " +" │ ( i ) or ( I ) increase log section height │ " +" │ ( j ) toggle log section visibility │ " +" │ ( a ) or ( A ) close dialog │ " +" │ ( k ) or ( K ) quit at any time │ " +" │ │ " +" │ currently an early work in progress, all and any input appreciated │ " +" │ https://github.com/mrjackwills/oxker │ " +" │ │ " +" ╰────────────────────────────────────────────────────────────────────────────────────╯ " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_show_timezone.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_show_timezone.snap index 291cc9f..ceed130 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_show_timezone.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_show_timezone.snap @@ -20,6 +20,7 @@ expression: setup.terminal.backend() " │ │ " " │ ( tab ) or ( shift+tab ) change panels │ " " │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) change selected line │ " +" │ ( ← → ) horizontal scroll across logs │ " " │ ( enter ) send docker container command │ " " │ ( e ) exec into a container │ " " │ ( h ) toggle this help information - or click heading │ " @@ -36,6 +37,5 @@ expression: setup.terminal.backend() " │ currently an early work in progress, all and any input appreciated │ " " │ https://github.com/mrjackwills/oxker │ " " │ │ " -" │ │ " " ╰───────────────────────────────────────────────────────────────────────────────────╯ " " " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_help_panel.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_help_panel.snap index ed89388..ad38e21 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_help_panel.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_help_panel.snap @@ -5,22 +5,22 @@ expression: setup.terminal.backend() " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) exit help " "╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮" "│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │" Hidden by multi-width symbols: [(2, " ")] -"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB ││ restart │" -"│ container_3 ✓ running Up 3 ho╭ 0.00.000 ──────────────────────────────────────────────────────────────────────────╮ ││ stop │" -"│ │ │ ││ delete │" +"│ container_2 ✓ running Up 2 ho╭ 0.00.000 ──────────────────────────────────────────────────────────────────────────╮ ││ restart │" +"│ container_3 ✓ running Up 3 ho│ │ ││ stop │" +"│ │ 88 │ ││ delete │" "│ │ 88 │ ││ │" "╰────────────────────────────────────│ 88 │────────────────────╯╰──────────────╯" -"╭ Logs 3/3 - container_1 - image_1 ──│ 88 │────────────────────────────────────╮" -"│ line 1 │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ │" -"│ line 2 │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ │" -"│▶ line 3 │ 8b d8 )888( 8888[ 8PP""""""" 88 │ │" -"│ │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ │" +"╭ Logs 3/3 - container_1 - image_1 ──│ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │────────────────────────────────────╮" +"│ line 1 │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ │" +"│ line 2 │ 8b d8 )888( 8888[ 8PP""""""" 88 │ │" +"│▶ line 3 │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ │" "│ │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ │" "│ │ │ │" "│ │ A simple tui to view & control docker containers │ │" "│ │ │ │" "│ │ ( tab ) or ( shift+tab ) change panels │ │" "│ │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) change selected line │ │" +"│ │ ( ← → ) horizontal scroll across logs │ │" "│ │ ( enter ) send docker container command │ │" "│ │ ( e ) exec into a container │ │" "│ │ ( h ) toggle this help information - or click heading │ │" @@ -37,8 +37,8 @@ expression: setup.terminal.backend() "│ │ • • │ currently an early work in progress, all and any input appreciated │ ││ 8001 │" "│ │ •• • │ https://github.com/mrjackwills/oxker │ ││127.0.0.1 8003 8003│" "│ │ • • │ │ ││ │" -"│ │ •• • • ╰────────────────────────────────────────────────────────────────────────────────────╯ ││ │" -"│ │• •• ││ │• •• ││ │" +"│ │ •• • • │ │ ││ │" +"│ │• •• ╰────────────────────────────────────────────────────────────────────────────────────╯ ││ │" "│ │• • ││ │• • ││ │" "│ │ ││ │ ││ │" "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯" diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs index e16e720..5915363 100644 --- a/src/ui/gui_state.rs +++ b/src/ui/gui_state.rs @@ -187,6 +187,7 @@ pub struct GuiState { log_height: u16, rerender: Arc, selected_panel: SelectablePanel, + screen_width: u16, show_logs: bool, status: HashSet, pub info_box_text: Option<(String, Instant)>, @@ -205,6 +206,7 @@ impl GuiState { loading_index: 0, loading_set: HashSet::new(), log_height: 75, + screen_width: 0, rerender: Arc::clone(redraw), selected_panel: SelectablePanel::default(), show_logs, @@ -232,6 +234,16 @@ impl GuiState { } } + /// Set the screen width, used for offset char calculations + pub const fn set_screen_width(&mut self, width: u16) { + self.screen_width = width; + } + + /// Get the screen width, used for offset char calculations + pub const fn get_screen_width(&self) -> u16 { + self.screen_width + } + pub const fn get_show_logs(&self) -> bool { self.show_logs } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index de55e09..412a6be 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -205,6 +205,10 @@ impl Ui { let docker_interval_ms = u128::from(self.app_data.lock().config.docker_interval_ms); let mut drawn_at = std::time::Instant::now(); + if let Ok(size) = self.terminal.size() { + self.gui_state.lock().set_screen_width(size.width); + } + while self.is_running.load(Ordering::SeqCst) { if self.should_redraw(&mut drawn_at, docker_interval_ms) { let fd = FrameData::from(&*self); @@ -243,11 +247,14 @@ impl Ui { } _ => (), } - } else if let Event::Resize(_, _) = event { + } else if let Event::Resize(width, _) = event { self.gui_state.lock().clear_area_map(); + // self.gui_state.lock().set_window_height(row); self.terminal.autoresize().ok(); + // todo set screen width + self.gui_state.lock().set_screen_width(width); } } } @@ -279,7 +286,6 @@ pub struct FrameData { filter_by: FilterBy, filter_term: Option, has_containers: bool, - // container_section_height: u16, log_height: u16, show_logs: bool, has_error: Option, @@ -290,6 +296,7 @@ pub struct FrameData { port_max_lens: (usize, usize, usize), ports: Option<(Vec, State)>, selected_panel: SelectablePanel, + scroll_title: Option, sorted_by: Option<(Header, SortedOrder)>, status: HashSet, } @@ -317,6 +324,7 @@ impl From<&Ui> for FrameData { log_title: app_data.get_log_title(), port_max_lens: app_data.get_longest_port(), ports: app_data.get_selected_ports(), + scroll_title: app_data.get_scroll_title(), selected_panel: gui_data.get_selected_panel(), sorted_by: app_data.get_sorted(), status: gui_data.get_status(), From cfda2ee57d44dc7dcafd8736f04d9fa54233a62a Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 14 Aug 2025 23:43:01 +0000 Subject: [PATCH 02/20] docs: readme.md updated --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 83837ae..2274537 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ In application controls, these, amongst many other settings, can be customized w |--|--| | ```( 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.| +| ```( ← → )``` | When logs panel selected, scroll horizontally across the text of the logs.| | ```( enter )```| Run selected docker command.| | ```( 1-9 )``` | Sort containers by heading, clicking on headings also sorts the selected column. | | ```( 0 )``` | Stop sorting.| From 79d19ceeb81ae60bc5562683e405d6e74e6f2578 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 14 Aug 2025 23:51:38 +0000 Subject: [PATCH 03/20] chore: Rust 1.89 linting #![allow(clippy::collapsible_if)] used, due to Zigbuild version @ 1.87.0 --- src/main.rs | 2 ++ src/ui/draw_blocks/filter.rs | 2 +- src/ui/draw_blocks/mod.rs | 22 +++++++++++----------- src/ui/draw_blocks/ports.rs | 2 +- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/main.rs b/src/main.rs index 66c642b..e7b77e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +#![allow(clippy::collapsible_if)] + use app_data::AppData; use app_error::AppError; use bollard::{API_DEFAULT_VERSION, Docker}; diff --git a/src/ui/draw_blocks/filter.rs b/src/ui/draw_blocks/filter.rs index cf62973..987a46e 100644 --- a/src/ui/draw_blocks/filter.rs +++ b/src/ui/draw_blocks/filter.rs @@ -8,7 +8,7 @@ use ratatui::{ use crate::{app_data::FilterBy, config::AppColors, ui::FrameData}; /// Create the filter_by by spans, coloured dependant on which one is selected -fn filter_by_spans(colors: AppColors, fd: &FrameData) -> [Span; 4] { +fn filter_by_spans(colors: AppColors, fd: &'_ FrameData) -> [Span<'_>; 4] { let selected = Style::default() .bg(colors.filter.selected_filter_background) .fg(colors.filter.selected_filter_text); diff --git a/src/ui/draw_blocks/mod.rs b/src/ui/draw_blocks/mod.rs index f97e753..56f829d 100644 --- a/src/ui/draw_blocks/mod.rs +++ b/src/ui/draw_blocks/mod.rs @@ -228,9 +228,9 @@ pub mod tests { /// Just a shorthand for when enumerating over result cells pub fn get_result( - setup: &TuiTestSetup, + setup: &'_ TuiTestSetup, // w: u16, - ) -> std::iter::Enumerate> { + ) -> std::iter::Enumerate> { setup .terminal .backend() @@ -284,7 +284,7 @@ pub mod tests { setup.app_data.lock().containers.items[0] .ports .push(ContainerPorts { - ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)), private: 8003, public: Some(8003), }); @@ -313,7 +313,7 @@ pub mod tests { setup.app_data.lock().containers.items[1] .ports .push(ContainerPorts { - ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)), private: 8003, public: Some(8003), }); @@ -348,7 +348,7 @@ pub mod tests { setup.app_data.lock().containers.items[0] .ports .push(ContainerPorts { - ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)), private: 8003, public: Some(8003), }); @@ -381,7 +381,7 @@ pub mod tests { setup.app_data.lock().containers.items[0] .ports .push(ContainerPorts { - ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)), private: 8003, public: Some(8003), }); @@ -410,7 +410,7 @@ pub mod tests { setup.app_data.lock().containers.items[0] .ports .push(ContainerPorts { - ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)), private: 8003, public: Some(8003), }); @@ -442,7 +442,7 @@ pub mod tests { setup.app_data.lock().containers.items[0] .ports .push(ContainerPorts { - ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)), private: 8003, public: Some(8003), }); @@ -472,7 +472,7 @@ pub mod tests { setup.app_data.lock().containers.items[0] .ports .push(ContainerPorts { - ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)), private: 8003, public: Some(8003), }); @@ -506,7 +506,7 @@ pub mod tests { setup.app_data.lock().containers.items[0] .ports .push(ContainerPorts { - ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)), private: 8003, public: Some(8003), }); @@ -538,7 +538,7 @@ pub mod tests { setup.app_data.lock().containers.items[0] .ports .push(ContainerPorts { - ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)), private: 8003, public: Some(8003), }); diff --git a/src/ui/draw_blocks/ports.rs b/src/ui/draw_blocks/ports.rs index fee6dff..44fd16d 100644 --- a/src/ui/draw_blocks/ports.rs +++ b/src/ui/draw_blocks/ports.rs @@ -182,7 +182,7 @@ mod tests { setup.app_data.lock().containers.items[0] .ports .push(ContainerPorts { - ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)), private: 8003, public: Some(8003), }); From eb686e2c952e04da74b3e12c0bfa015ec4615e1d Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 14 Aug 2025 23:55:04 +0000 Subject: [PATCH 04/20] refactor: remove macos cfg conts functions Zigbuild updated to rust 1.87 --- src/app_data/container_state.rs | 24 ------------------------ src/app_data/mod.rs | 7 ------- 2 files changed, 31 deletions(-) diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index 97e9f42..ed605f0 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -27,14 +27,6 @@ impl From<&str> for ContainerId { } impl ContainerId { - // TODO remove this once zigbuild uses Rust v1.87.0 - #[cfg(target_os = "macos")] - #[allow(clippy::missing_const_for_fn)] - pub fn get(&self) -> &str { - self.0.as_str() - } - - #[cfg(not(target_os = "macos"))] pub const fn get(&self) -> &str { self.0.as_str() } @@ -81,14 +73,6 @@ macro_rules! unit_struct { } impl $name { - #[cfg(target_os = "macos")] - #[allow(clippy::missing_const_for_fn)] - // TODO remove this once zigbuild uses Rust v1.87.0 - pub fn get(&self) -> &str { - self.0.as_str() - } - - #[cfg(not(target_os = "macos"))] pub const fn get(&self) -> &str { self.0.as_str() } @@ -706,14 +690,6 @@ impl Logs { self.lines.start(); } - // // TODO remove this once zigbuild uses Rust v1.87.0 - // #[cfg(target_os = "macos")] - // #[allow(clippy::missing_const_for_fn)] - // pub fn len(&self) -> usize { - // self.logs.items.len() - // } - - // #[cfg(not(target_os = "macos"))] pub const fn len(&self) -> usize { self.lines.items.len() } diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index a6e17af..0b97449 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -407,13 +407,6 @@ impl AppData { /// Container state methods /// Get the total number of none "hidden" containers - // TODO remove this once zigbuild uses Rust v1.87.0 - #[cfg(target_os = "macos")] - pub fn get_container_len(&self) -> usize { - self.containers.items.len() - } - - #[cfg(not(target_os = "macos"))] pub const fn get_container_len(&self) -> usize { self.containers.items.len() } From 50edbc0cc09db864835fe81a03cba8eadafe548b Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Fri, 15 Aug 2025 01:07:34 +0000 Subject: [PATCH 05/20] feat: clear screen & redraw New keymap key to clear the screen & redraw. Useful if gui shows any glitches --- README.md | 1 + example_config/example.config.jsonc | 4 + example_config/example.config.toml | 2 + src/app_data/mod.rs | 52 +++--- src/config/config.toml | 2 + src/config/keymap_parser.rs | 27 +-- src/input_handler/mod.rs | 5 + src/main.rs | 2 +- src/ui/draw_blocks/help.rs | 163 +++++++++--------- ...blocks__help__tests__draw_blocks_help.snap | 1 + ...tests__draw_blocks_help_custom_colors.snap | 1 + ...cks_help_custom_keymap_one_definition.snap | 55 +++--- ...ks_help_custom_keymap_two_definitions.snap | 1 + ...w_blocks_help_one_and_two_definitions.snap | 99 +++++------ ...tests__draw_blocks_help_show_timezone.snap | 4 +- ...__draw_blocks_whole_layout_help_panel.snap | 14 +- src/ui/gui_state.rs | 33 ++-- src/ui/mod.rs | 25 ++- src/ui/redraw.rs | 33 +++- 19 files changed, 294 insertions(+), 230 deletions(-) diff --git a/README.md b/README.md index 2274537..fb9ef37 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ In application controls, these, amongst many other settings, can be customized w | ```( - ) ``` or ```(=)``` | Reduce or increase the height of the logs panel.| | ```( \ )``` | Toggle the visibility of the logs panel.| | ```( e )``` | Exec into the selected container - not available on Windows.| +| ```( f )``` | Force clear the screen & redraw the gui.| | ```( h )``` | Toggle help menu.| | ```( m )``` | Toggle mouse capture - if disabled, text on screen can be selected.| | ```( q )``` | Quit.| diff --git a/example_config/example.config.jsonc b/example_config/example.config.jsonc index af38848..bf0436a 100644 --- a/example_config/example.config.jsonc +++ b/example_config/example.config.jsonc @@ -166,6 +166,10 @@ // Toggle visibility of the log section "log_section_toggle": [ "\\" + ], + // Force a complete clear & redraw of the screen + "force_redraw": [ + "f" ] }, //////////////////// diff --git a/example_config/example.config.toml b/example_config/example.config.toml index 46ff71f..a7132ac 100644 --- a/example_config/example.config.toml +++ b/example_config/example.config.toml @@ -115,6 +115,8 @@ log_section_height_decrease = ["-"] log_section_height_increase = ["+"] # Toggle visibility of the log section log_section_toggle = ["\\"] +# Force a complete clear & redraw of the screen +force_redraw = ["f"] ################# # Custom Colors # diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 0b97449..f8508ad 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -122,7 +122,7 @@ pub struct AppData { error: Option, filter: Filter, hidden_containers: Vec, - redraw: Arc, + rerender: Arc, sorted_by: Option<(Header, SortedOrder)>, current_sorted_id: Vec, pub config: Config, @@ -137,7 +137,7 @@ pub struct AppData { pub filter: Filter, pub hidden_containers: Vec, pub current_sorted_id: Vec, - pub redraw: Arc, + pub rerender: Arc, pub sorted_by: Option<(Header, SortedOrder)>, } @@ -151,7 +151,7 @@ impl AppData { error: None, filter: Filter::new(), hidden_containers: vec![], - redraw: Arc::clone(redraw), + rerender: Arc::clone(redraw), sorted_by: None, } } @@ -192,7 +192,7 @@ impl AppData { /// sets the state to start if any filtering has occurred /// Also search in the "hidden" vec for items and insert back into the main containers vec fn filter_containers(&mut self) { - self.redraw.update(); + self.rerender.update_draw(); let pre_len = self.get_container_len(); if !self.hidden_containers.is_empty() { @@ -296,7 +296,7 @@ impl AppData { /// Remove the sorted header & order, and sort by default - created datetime pub fn reset_sorted(&mut self) { self.set_sorted(None); - self.redraw.update(); + self.rerender.update_draw(); } /// Sort containers based on a given header, if headings match, and already ascending, remove sorting @@ -392,7 +392,7 @@ impl AppData { self.containers.items.sort_by(sort_closure); if pre_order != self.get_current_ids() { - self.redraw.update(); + self.rerender.update_draw(); } } else if self.current_sorted_id != self.get_current_ids() { self.containers.items.sort_by(|a, b| { @@ -400,7 +400,7 @@ impl AppData { .cmp(&b.created) .then_with(|| a.name.get().cmp(b.name.get())) }); - self.redraw.update(); + self.rerender.update_draw(); self.current_sorted_id = self.get_current_ids(); } } @@ -439,25 +439,25 @@ impl AppData { /// Select the first container pub fn containers_start(&mut self) { self.containers.start(); - self.redraw.update(); + self.rerender.update_draw(); } /// select the last container pub fn containers_end(&mut self) { self.containers.end(); - self.redraw.update(); + self.rerender.update_draw(); } /// Select the next container pub fn containers_next(&mut self) { self.containers.next(); - self.redraw.update(); + self.rerender.update_draw(); } /// select the previous container pub fn containers_previous(&mut self) { self.containers.previous(); - self.redraw.update(); + self.rerender.update_draw(); } /// Get ListState of containers @@ -579,7 +579,7 @@ impl AppData { pub fn docker_controls_next(&mut self) { if let Some(i) = self.get_mut_selected_container() { i.docker_controls.next(); - self.redraw.update(); + self.rerender.update_draw(); } } @@ -587,7 +587,7 @@ impl AppData { pub fn docker_controls_previous(&mut self) { if let Some(i) = self.get_mut_selected_container() { i.docker_controls.previous(); - self.redraw.update(); + self.rerender.update_draw(); } } @@ -595,7 +595,7 @@ impl AppData { pub fn docker_controls_start(&mut self) { if let Some(i) = self.get_mut_selected_container() { i.docker_controls.start(); - self.redraw.update(); + self.rerender.update_draw(); } } @@ -603,7 +603,7 @@ impl AppData { pub fn docker_controls_end(&mut self) { if let Some(i) = self.get_mut_selected_container() { i.docker_controls.end(); - self.redraw.update(); + self.rerender.update_draw(); } } @@ -647,7 +647,7 @@ impl AppData { pub fn log_back(&mut self) { if let Some(i) = self.get_mut_selected_container() { i.logs.back(); - self.redraw.update(); + self.rerender.update_draw(); } } @@ -655,7 +655,7 @@ impl AppData { pub fn log_forward(&mut self, width: u16) { if let Some(i) = self.get_mut_selected_container() { i.logs.forward(width); - self.redraw.update(); + self.rerender.update_draw(); } } @@ -663,7 +663,7 @@ impl AppData { pub fn log_next(&mut self) { if let Some(i) = self.get_mut_selected_container() { i.logs.next(); - self.redraw.update(); + self.rerender.update_draw(); } } @@ -671,7 +671,7 @@ impl AppData { pub fn log_previous(&mut self) { if let Some(i) = self.get_mut_selected_container() { i.logs.previous(); - self.redraw.update(); + self.rerender.update_draw(); } } @@ -679,7 +679,7 @@ impl AppData { pub fn log_end(&mut self) { if let Some(i) = self.get_mut_selected_container() { i.logs.end(); - self.redraw.update(); + self.rerender.update_draw(); } } @@ -687,7 +687,7 @@ impl AppData { pub fn log_start(&mut self) { if let Some(i) = self.get_mut_selected_container() { i.logs.start(); - self.redraw.update(); + self.rerender.update_draw(); } } @@ -728,14 +728,14 @@ impl AppData { /// Remove single app_state error pub fn remove_error(&mut self) { self.error = None; - self.redraw.update(); + self.rerender.update_draw(); } /// Insert single app_state error pub fn set_error(&mut self, error: AppError, gui_state: &Arc>, status: Status) { gui_state.lock().status_push(status); self.error = Some(error); - self.redraw.update(); + self.rerender.update_draw(); } /// Check if the selected container is a dockerised version of oxker @@ -825,7 +825,7 @@ impl AppData { container.mem_limit.update(mem_limit); } if self.is_selected_container(id) { - self.redraw.update(); + self.rerender.update_draw(); } self.sort_containers(); } @@ -863,7 +863,7 @@ impl AppData { if self.containers.items.get(index).is_some() { self.containers.items.remove(index); if self.is_selected_container(id) { - self.redraw.update(); + self.rerender.update_draw(); } } } @@ -992,7 +992,7 @@ impl AppData { } } if self.is_selected_container(id) { - self.redraw.update(); + self.rerender.update_draw(); } } } diff --git a/src/config/config.toml b/src/config/config.toml index 37dbd13..25ee594 100644 --- a/src/config/config.toml +++ b/src/config/config.toml @@ -114,6 +114,8 @@ log_section_height_decrease = ["-"] log_section_height_increase = ["+"] # Toggle visibility of the log section log_section_toggle = ["\\"] +# Force a complete clear & redraw of the screen +force_redraw = ["f"] ################# # Custom Colors # diff --git a/src/config/keymap_parser.rs b/src/config/keymap_parser.rs index e66acef..808cc18 100644 --- a/src/config/keymap_parser.rs +++ b/src/config/keymap_parser.rs @@ -35,6 +35,7 @@ macro_rules! config_struct { optional_config_struct!( ConfigKeymap, clear, + force_redraw, delete_deny, delete_confirm, exec, @@ -75,6 +76,7 @@ config_struct!( delete_confirm, exec, filter_mode, + force_redraw, log_section_height_increase, log_section_height_decrease, log_section_toggle, @@ -112,6 +114,7 @@ impl Keymap { delete_deny: (KeyCode::Char('n'), None), exec: (KeyCode::Char('e'), None), filter_mode: (KeyCode::Char('/'), Some(KeyCode::F(1))), + force_redraw: (KeyCode::Char('f'), None), log_section_height_decrease: (KeyCode::Char('-'), None), log_section_height_increase: (KeyCode::Char('='), None), log_section_toggle: (KeyCode::Char('\\'), None), @@ -195,6 +198,7 @@ impl From> for Keymap { update_keymap(ck.exec, &mut keymap.exec, &mut clash); update_keymap(ck.filter_mode, &mut keymap.filter_mode, &mut clash); + update_keymap(ck.force_redraw, &mut keymap.force_redraw, &mut clash); update_keymap(ck.quit, &mut keymap.quit, &mut clash); update_keymap(ck.save_logs, &mut keymap.save_logs, &mut clash); update_keymap( @@ -376,11 +380,13 @@ mod tests { delete_deny: Some(vec!["s".to_owned()]), delete_confirm: None, exec: None, + filter_mode: None, + force_redraw: None, + log_scroll_back: None, + log_scroll_forward: None, log_section_height_decrease: None, log_section_height_increase: None, - log_scroll_forward: None, - log_scroll_back: None, - filter_mode: None, + log_section_toggle: None, quit: None, save_logs: None, scroll_down_many: None, @@ -390,16 +396,15 @@ mod tests { scroll_up_many: None, scroll_up_one: None, select_next_panel: None, - log_section_toggle: None, select_previous_panel: None, - sort_by_name: None, - sort_by_state: None, - sort_by_status: None, sort_by_cpu: None, - sort_by_memory: None, sort_by_id: None, sort_by_image: None, + sort_by_memory: None, + sort_by_name: None, sort_by_rx: None, + sort_by_state: None, + sort_by_status: None, sort_by_tx: None, sort_reset: None, toggle_help: None, @@ -422,6 +427,7 @@ mod tests { delete_deny: gen_v(("c", "d")), exec: gen_v(("g", "h")), filter_mode: gen_v(("i", "j")), + force_redraw: gen_v(("F1", "F2")), log_section_height_decrease: gen_v(("-", "Z")), log_section_height_increase: gen_v(("=", "X")), log_scroll_forward: gen_v(("right", "R")), @@ -437,7 +443,7 @@ mod tests { scroll_up_one: gen_v(("y", "z")), select_next_panel: gen_v(("0", "1")), select_previous_panel: gen_v(("2", "3")), - sort_by_cpu: gen_v(("F1", "F12")), + sort_by_cpu: gen_v(("F11", "F12")), sort_by_id: gen_v(("[", "]")), sort_by_image: gen_v(("A", "B")), sort_by_memory: gen_v(("/", "\\")), @@ -457,6 +463,7 @@ mod tests { clear: (KeyCode::Char('a'), Some(KeyCode::Char('b'))), delete_deny: (KeyCode::Char('c'), Some(KeyCode::Char('d'))), delete_confirm: (KeyCode::Char('e'), Some(KeyCode::Char('f'))), + force_redraw: (KeyCode::F(1), Some(KeyCode::F(2))), log_section_height_decrease: (KeyCode::Char('-'), Some(KeyCode::Char('Z'))), log_section_height_increase: (KeyCode::Char('='), Some(KeyCode::Char('X'))), log_section_toggle: (KeyCode::Char('Y'), Some(KeyCode::Char('W'))), @@ -477,7 +484,7 @@ mod tests { sort_by_name: (KeyCode::Char('4'), Some(KeyCode::Char('5'))), sort_by_state: (KeyCode::Char('6'), Some(KeyCode::Char('7'))), sort_by_status: (KeyCode::Char('8'), Some(KeyCode::Char('9'))), - sort_by_cpu: (KeyCode::F(1), Some(KeyCode::F(12))), + sort_by_cpu: (KeyCode::F(11), Some(KeyCode::F(12))), sort_by_memory: (KeyCode::Char('/'), Some(KeyCode::Char('\\'))), sort_by_id: (KeyCode::Char('['), Some(KeyCode::Char(']'))), sort_by_image: (KeyCode::Char('A'), Some(KeyCode::Char('B'))), diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 1d5c22b..d45b472 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -408,6 +408,11 @@ impl InputHandler { /// Handle input that refers to the sorting of columns fn handle_sort(&self, key_code: KeyCode) { match key_code { + _ if self.keymap.force_redraw.0 == key_code + || self.keymap.force_redraw.1 == Some(key_code) => + { + self.gui_state.lock().set_clear(); + } _ if self.keymap.sort_reset.0 == key_code || self.keymap.sort_reset.1 == Some(key_code) => { diff --git a/src/main.rs b/src/main.rs index e7b77e5..665aa4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -210,7 +210,7 @@ mod tests { current_sorted_id: vec![], error: None, sorted_by: None, - redraw: Arc::new(Rerender::new()), + rerender: Arc::new(Rerender::new()), filter: Filter::new(), config: gen_config(), } diff --git a/src/ui/draw_blocks/help.rs b/src/ui/draw_blocks/help.rs index c861996..0694ddd 100644 --- a/src/ui/draw_blocks/help.rs +++ b/src/ui/draw_blocks/help.rs @@ -84,7 +84,6 @@ impl HelpInfo { } } - // todo ← → for log moving /// Generate the button information span + metadata #[allow(clippy::too_many_lines)] fn gen_keymap_info(colors: AppColors, zone: Option<&TimeZone>, show_timestamp: bool) -> Self { @@ -129,6 +128,11 @@ impl HelpInfo { #[cfg(target_os = "windows")] button_desc(" - not available on Windows"), ]), + Line::from(vec![ + space(), + button_item("f"), + button_desc("force clear the screen & redraw the gui"), + ]), Line::from(vec![ space(), button_item("h"), @@ -285,6 +289,7 @@ impl HelpInfo { or_secondary(km.exec, "exec into a container"), #[cfg(target_os = "windows")] or_secondary(km.exec, "exec into a container - not available on Windows"), + or_secondary(km.force_redraw, "force clear the screen & redraw the gui"), or_secondary( km.toggle_help, "toggle this help information - or click heading", @@ -445,9 +450,8 @@ mod tests { #[test] /// This will cause issues once the version has more than the current 5 chars (0.5.0) /// println!("{} {} {} {} {}", row_index, result_cell_index, result_cell.symbol(), result_cell.bg, result_cell.fg); - /// TODO broken wihh the horizonal scrolls! fn test_draw_blocks_help() { - let mut setup = test_setup(87, 35, true, true); + let mut setup = test_setup(87, 36, true, true); let tz = setup.app_data.lock().config.timezone.clone(); setup @@ -469,12 +473,12 @@ mod tests { for (result_cell_index, result_cell) in result_row.iter().enumerate() { match (row_index, result_cell_index) { // first & last row, and first & last char on each row, is reset/reset, making sure that the help info is centered in the given area - (0 | 34, _) | (0..=33, 0 | 86) => { + (0 | 35, _) | (0..=34, 0 | 86) => { assert_eq!(result_cell.bg, Color::Reset); assert_eq!(result_cell.fg, Color::Reset); } // border is red on black - (1 | 32, _) | (1..=31, 1 | 85) => { + (1 | 34, _) | (1..=31, 1 | 85) => { assert_eq!(result_cell.bg, Color::Magenta); assert_eq!(result_cell.fg, Color::Black); } @@ -483,15 +487,15 @@ mod tests { | (12, 19..=66) | (14, 2..=10 | 13..=27) | (15, 2..=10 | 13..=21 | 24..=40 | 43..=56) - | (16 | 25 | 27, 2..=10) - | (17 | 24, 2..=12) - | (18 | 19 | 20 | 21 | 23 | 26 | 28, 2..=8) - | (22, 2..=9 | 12..=18) => { + | (16 | 26 | 28, 2..=10) + | (17 | 25, 2..=12) + | (18 | 19 | 20 | 21 | 22 | 24 | 27 | 29, 2..=8) + | (23, 2..=9 | 12..=18) => { assert_eq!(result_cell.bg, Color::Magenta); assert_eq!(result_cell.fg, Color::White); } // The URL is yellow and underlined - (31, 25..=60) => { + (32, 25..=60) => { assert_eq!(result_cell.bg, Color::Magenta); assert_eq!(result_cell.fg, Color::White); assert_eq!(result_cell.modifier, Modifier::UNDERLINED); @@ -508,10 +512,9 @@ mod tests { #[test] /// Test that the help panel gets drawn with custom colors - /// This test is annoying /// println!("{} {} {} {} {}", row_index, result_cell_index, result_cell.symbol(), result_cell.bg, result_cell.fg); fn test_draw_blocks_help_custom_colors() { - let mut setup = test_setup(87, 35, true, true); + let mut setup = test_setup(87, 36, true, true); let mut colors = AppColors::new(); let tz = setup.app_data.lock().config.timezone.clone(); @@ -533,17 +536,16 @@ mod tests { .unwrap(); assert_snapshot!(setup.terminal.backend()); - for (row_index, result_row) in get_result(&setup) { for (result_cell_index, result_cell) in result_row.iter().enumerate() { match (row_index, result_cell_index) { // first & last row, and first & last char on each row, is reset/reset, making sure that the help info is centered in the given area - (0 | 34, _) | (0..=33, 0 | 86) => { + (0 | 35, _) | (0..=34, 0 | 86) => { assert_eq!(result_cell.bg, Color::Reset); assert_eq!(result_cell.fg, Color::Reset); } // border is red on black - (1 | 32, _) | (1..=31, 1 | 85) => { + (1 | 34, _) | (1..=31, 1 | 85) => { assert_eq!(result_cell.bg, Color::Black); assert_eq!(result_cell.fg, Color::Red); } @@ -552,15 +554,15 @@ mod tests { | (12, 19..=66) | (14, 2..=10 | 13..=27) | (15, 2..=10 | 13..=21 | 24..=40 | 43..=56) - | (16 | 25 | 27, 2..=10) - | (17 | 24, 2..=12) - | (18 | 19 | 20 | 21 | 23 | 26 | 28, 2..=8) - | (22, 2..=9 | 12..=18) => { + | (16 | 26 | 28, 2..=10) + | (17 | 25, 2..=12) + | (18 | 19 | 20 | 21 | 22 | 24 | 27 | 29, 2..=8) + | (23, 2..=9 | 12..=18) => { assert_eq!(result_cell.bg, Color::Black); assert_eq!(result_cell.fg, Color::Yellow); } // The URL is yellow and underlined - (31, 25..=60) => { + (32, 25..=60) => { assert_eq!(result_cell.bg, Color::Black); assert_eq!(result_cell.fg, Color::Yellow); assert_eq!(result_cell.modifier, Modifier::UNDERLINED); @@ -578,7 +580,7 @@ mod tests { #[test] /// Help panel will show custom keymap if in use, with one definition for each entry fn test_draw_blocks_help_custom_keymap_one_definition() { - let mut setup = test_setup(98, 49, true, true); + let mut setup = test_setup(98, 50, true, true); let input = Keymap { clear: (KeyCode::Char('a'), None), @@ -586,33 +588,34 @@ mod tests { delete_deny: (KeyCode::Char('c'), None), exec: (KeyCode::Char('d'), None), filter_mode: (KeyCode::Char('e'), None), - log_scroll_back: (KeyCode::Char('f'), None), - log_scroll_forward: (KeyCode::Char('g'), None), - log_section_height_decrease: (KeyCode::Char('h'), None), - log_section_height_increase: (KeyCode::Char('i'), None), - log_section_toggle: (KeyCode::Char('j'), None), - quit: (KeyCode::Char('k'), None), - save_logs: (KeyCode::Char('l'), None), - scroll_down_many: (KeyCode::Char('m'), None), - scroll_down_one: (KeyCode::Char('n'), None), - scroll_end: (KeyCode::Char('o'), None), - scroll_start: (KeyCode::Char('p'), None), - scroll_up_many: (KeyCode::Char('q'), None), - scroll_up_one: (KeyCode::Char('r'), None), - select_next_panel: (KeyCode::Char('s'), None), - select_previous_panel: (KeyCode::Char('t'), None), - sort_by_cpu: (KeyCode::Char('u'), None), - sort_by_id: (KeyCode::Char('v'), None), - sort_by_image: (KeyCode::Char('w'), None), - sort_by_memory: (KeyCode::Char('x'), None), - sort_by_name: (KeyCode::Char('y'), None), - sort_by_rx: (KeyCode::Char('z'), None), - sort_by_state: (KeyCode::Char('0'), None), - sort_by_status: (KeyCode::Char('1'), None), - sort_by_tx: (KeyCode::Char('2'), None), - sort_reset: (KeyCode::Char('3'), None), - toggle_help: (KeyCode::Char('4'), None), - toggle_mouse_capture: (KeyCode::Char('5'), None), + force_redraw: (KeyCode::Char('f'), None), + log_scroll_back: (KeyCode::Char('g'), None), + log_scroll_forward: (KeyCode::Char('h'), None), + log_section_height_decrease: (KeyCode::Char('i'), None), + log_section_height_increase: (KeyCode::Char('j'), None), + log_section_toggle: (KeyCode::Char('k'), None), + quit: (KeyCode::Char('l'), None), + save_logs: (KeyCode::Char('m'), None), + scroll_down_many: (KeyCode::Char('n'), None), + scroll_down_one: (KeyCode::Char('o'), None), + scroll_end: (KeyCode::Char('p'), None), + scroll_start: (KeyCode::Char('q'), None), + scroll_up_many: (KeyCode::Char('r'), None), + scroll_up_one: (KeyCode::Char('s'), None), + select_next_panel: (KeyCode::Char('t'), None), + select_previous_panel: (KeyCode::Char('u'), None), + sort_by_cpu: (KeyCode::Char('v'), None), + sort_by_id: (KeyCode::Char('w'), None), + sort_by_image: (KeyCode::Char('x'), None), + sort_by_memory: (KeyCode::Char('y'), None), + sort_by_name: (KeyCode::Char('z'), None), + sort_by_rx: (KeyCode::Char('0'), None), + sort_by_state: (KeyCode::Char('1'), None), + sort_by_status: (KeyCode::Char('2'), None), + sort_by_tx: (KeyCode::Char('3'), None), + sort_reset: (KeyCode::Char('4'), None), + toggle_help: (KeyCode::Char('5'), None), + toggle_mouse_capture: (KeyCode::Char('6'), None), }; setup @@ -628,7 +631,7 @@ mod tests { #[test] /// Help panel will show custom keymap if in use, with two definition for each entry fn test_draw_blocks_help_custom_keymap_two_definitions() { - let mut setup = test_setup(110, 49, true, true); + let mut setup = test_setup(110, 50, true, true); let keymap = Keymap { clear: (KeyCode::Char('a'), Some(KeyCode::Char('A'))), @@ -636,6 +639,7 @@ mod tests { delete_deny: (KeyCode::Char('c'), Some(KeyCode::Char('C'))), exec: (KeyCode::Char('d'), Some(KeyCode::Char('D'))), filter_mode: (KeyCode::Char('e'), Some(KeyCode::Char('E'))), + force_redraw: (KeyCode::Char('f'), Some(KeyCode::Char('F'))), log_scroll_back: (KeyCode::Char('f'), Some(KeyCode::Char('F'))), log_scroll_forward: (KeyCode::Char('g'), Some(KeyCode::Char('G'))), log_section_height_decrease: (KeyCode::Char('h'), Some(KeyCode::Char('H'))), @@ -678,7 +682,7 @@ mod tests { #[test] /// Help panel will show custom keymap if in use, with either one or two definition for each entry fn test_draw_blocks_help_one_and_two_definitions() { - let mut setup = test_setup(110, 49, true, true); + let mut setup = test_setup(110, 50, true, true); let keymap = Keymap { clear: (KeyCode::Char('a'), Some(KeyCode::Char('A'))), @@ -686,33 +690,34 @@ mod tests { delete_deny: (KeyCode::Char('c'), Some(KeyCode::Char('C'))), exec: (KeyCode::Char('d'), None), filter_mode: (KeyCode::Char('e'), Some(KeyCode::Char('E'))), - log_scroll_back: (KeyCode::Char('f'), None), - log_scroll_forward: (KeyCode::Char('g'), Some(KeyCode::Char('G'))), - log_section_height_decrease: (KeyCode::Char('h'), None), - log_section_height_increase: (KeyCode::Char('i'), Some(KeyCode::Char('I'))), - log_section_toggle: (KeyCode::Char('j'), None), - quit: (KeyCode::Char('k'), Some(KeyCode::Char('K'))), - save_logs: (KeyCode::Char('l'), None), - scroll_down_many: (KeyCode::Char('m'), Some(KeyCode::Char('M'))), - scroll_down_one: (KeyCode::Char('n'), None), - scroll_end: (KeyCode::Char('o'), Some(KeyCode::Char('O'))), - scroll_start: (KeyCode::Char('p'), None), - scroll_up_many: (KeyCode::Char('q'), Some(KeyCode::Char('Q'))), - scroll_up_one: (KeyCode::Char('r'), None), - select_next_panel: (KeyCode::Char('s'), Some(KeyCode::Char('S'))), - select_previous_panel: (KeyCode::Char('t'), None), - sort_by_cpu: (KeyCode::Char('u'), Some(KeyCode::Char('U'))), - sort_by_id: (KeyCode::Char('v'), None), - sort_by_image: (KeyCode::Char('w'), Some(KeyCode::Char('W'))), - sort_by_memory: (KeyCode::Char('x'), None), - sort_by_name: (KeyCode::Char('y'), Some(KeyCode::Char('Y'))), - sort_by_rx: (KeyCode::Char('z'), None), - sort_by_state: (KeyCode::Char('0'), Some(KeyCode::Char('9'))), - sort_by_status: (KeyCode::Char('1'), None), - sort_by_tx: (KeyCode::Char('2'), Some(KeyCode::Char('7'))), - sort_reset: (KeyCode::Char('3'), None), - toggle_help: (KeyCode::Char('4'), Some(KeyCode::Char('5'))), - toggle_mouse_capture: (KeyCode::Char('5'), None), + force_redraw: (KeyCode::Char('f'), None), + log_scroll_back: (KeyCode::Char('g'), Some(KeyCode::Char('G'))), + log_scroll_forward: (KeyCode::Char('h'), None), + log_section_height_decrease: (KeyCode::Char('i'), Some(KeyCode::Char('I'))), + log_section_height_increase: (KeyCode::Char('j'), None), + log_section_toggle: (KeyCode::Char('k'), Some(KeyCode::Char('K'))), + quit: (KeyCode::Char('l'), None), + save_logs: (KeyCode::Char('m'), Some(KeyCode::Char('M'))), + scroll_down_many: (KeyCode::Char('n'), None), + scroll_down_one: (KeyCode::Char('o'), Some(KeyCode::Char('O'))), + scroll_end: (KeyCode::Char('p'), None), + scroll_start: (KeyCode::Char('q'), Some(KeyCode::Char('Q'))), + scroll_up_many: (KeyCode::Char('r'), None), + scroll_up_one: (KeyCode::Char('s'), Some(KeyCode::Char('S'))), + select_next_panel: (KeyCode::Char('t'), None), + select_previous_panel: (KeyCode::Char('u'), Some(KeyCode::Char('U'))), + sort_by_cpu: (KeyCode::Char('v'), None), + sort_by_id: (KeyCode::Char('w'), Some(KeyCode::Char('W'))), + sort_by_image: (KeyCode::Char('x'), None), + sort_by_memory: (KeyCode::Char('y'), Some(KeyCode::Char('Y'))), + sort_by_name: (KeyCode::Char('z'), None), + sort_by_rx: (KeyCode::Char('0'), Some(KeyCode::Char('9'))), + sort_by_state: (KeyCode::Char('1'), None), + sort_by_status: (KeyCode::Char('2'), Some(KeyCode::Char('7'))), + sort_by_tx: (KeyCode::Char('3'), None), + sort_reset: (KeyCode::Char('4'), Some(KeyCode::Char('5'))), + toggle_help: (KeyCode::Char('5'), None), + toggle_mouse_capture: (KeyCode::Char('6'), Some(KeyCode::Char('7'))), }; let tz = setup.app_data.lock().config.timezone.clone(); @@ -749,10 +754,10 @@ mod tests { for (row_index, result_row) in get_result(&setup) { for (result_cell_index, result_cell) in result_row.iter().enumerate() { match (row_index, result_cell_index) { - (14, 31..=45) => { + (13, 31..=45) => { assert_eq!(result_cell.fg, AppColors::new().popup_help.text); } - (14, 46..=55) => { + (13, 46..=55) => { assert_eq!(result_cell.fg, AppColors::new().popup_help.text_highlight); } _ => (), diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help.snap index 8b03bdd..de059d4 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help.snap @@ -21,6 +21,7 @@ expression: setup.terminal.backend() " │ ( ← → ) horizontal scroll across logs │ " " │ ( enter ) send docker container command │ " " │ ( e ) exec into a container │ " +" │ ( f ) force clear the screen & redraw the gui │ " " │ ( h ) toggle this help information - or click heading │ " " │ ( s ) save logs to file │ " " │ ( m ) toggle mouse capture - if disabled, text on screen can be selected & copied │ " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_colors.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_colors.snap index 8b03bdd..de059d4 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_colors.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_colors.snap @@ -21,6 +21,7 @@ expression: setup.terminal.backend() " │ ( ← → ) horizontal scroll across logs │ " " │ ( enter ) send docker container command │ " " │ ( e ) exec into a container │ " +" │ ( f ) force clear the screen & redraw the gui │ " " │ ( h ) toggle this help information - or click heading │ " " │ ( s ) save logs to file │ " " │ ( m ) toggle mouse capture - if disabled, text on screen can be selected & copied │ " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_one_definition.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_one_definition.snap index fe00d6a..eee704c 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_one_definition.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_one_definition.snap @@ -15,37 +15,38 @@ expression: setup.terminal.backend() " │ │ " " │ A simple tui to view & control docker containers │ " " │ │ " -" │ ( s ) select next panel │ " -" │ ( t ) select previous panel │ " -" │ ( n ) scroll list down by one │ " -" │ ( r ) scroll list up by one │ " -" │ ( m ) scroll list down by many │ " -" │ ( q ) scroll list by up many │ " -" │ ( o ) scroll list to end │ " -" │ ( p ) scroll list to start │ " -" │ ( g ) horizontal scroll logs right │ " -" │ ( f ) horizontal scroll logs left │ " +" │ ( t ) select next panel │ " +" │ ( u ) select previous panel │ " +" │ ( o ) scroll list down by one │ " +" │ ( s ) scroll list up by one │ " +" │ ( n ) scroll list down by many │ " +" │ ( r ) scroll list by up many │ " +" │ ( p ) scroll list to end │ " +" │ ( q ) scroll list to start │ " +" │ ( h ) horizontal scroll logs right │ " +" │ ( g ) horizontal scroll logs left │ " " │ ( enter ) send docker container command │ " " │ ( d ) exec into a container │ " -" │ ( 4 ) toggle this help information - or click heading │ " -" │ ( l ) save logs to file │ " -" │ ( 5 ) toggle mouse capture - if disabled, text on screen can be selected & copied │ " +" │ ( f ) force clear the screen & redraw the gui │ " +" │ ( 5 ) toggle this help information - or click heading │ " +" │ ( m ) save logs to file │ " +" │ ( 6 ) toggle mouse capture - if disabled, text on screen can be selected & copied │ " " │ ( e ) enter filter mode │ " -" │ ( 3 ) reset container sorting │ " -" │ ( y ) sort containers by name │ " -" │ ( 0 ) sort containers by state │ " -" │ ( 1 ) sort containers by status │ " -" │ ( u ) sort containers by cpu │ " -" │ ( x ) sort containers by memory │ " -" │ ( v ) sort containers by id │ " -" │ ( w ) sort containers by image │ " -" │ ( z ) sort containers by rx │ " -" │ ( 2 ) sort containers by tx │ " -" │ ( h ) decrease log section height │ " -" │ ( i ) increase log section height │ " -" │ ( j ) toggle log section visibility │ " +" │ ( 4 ) reset container sorting │ " +" │ ( z ) sort containers by name │ " +" │ ( 1 ) sort containers by state │ " +" │ ( 2 ) sort containers by status │ " +" │ ( v ) sort containers by cpu │ " +" │ ( y ) sort containers by memory │ " +" │ ( w ) sort containers by id │ " +" │ ( x ) sort containers by image │ " +" │ ( 0 ) sort containers by rx │ " +" │ ( 3 ) sort containers by tx │ " +" │ ( i ) decrease log section height │ " +" │ ( j ) increase log section height │ " +" │ ( k ) toggle log section visibility │ " " │ ( a ) close dialog │ " -" │ ( k ) quit at any time │ " +" │ ( l ) quit at any time │ " " │ │ " " │ currently an early work in progress, all and any input appreciated │ " " │ https://github.com/mrjackwills/oxker │ " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_two_definitions.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_two_definitions.snap index 6ae3453..418e684 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_two_definitions.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_two_definitions.snap @@ -27,6 +27,7 @@ expression: setup.terminal.backend() " │ ( f ) or ( F ) horizontal scroll logs left │ " " │ ( enter ) send docker container command │ " " │ ( d ) or ( D ) exec into a container │ " +" │ ( f ) or ( F ) force clear the screen & redraw the gui │ " " │ ( 4 ) or ( 5 ) toggle this help information - or click heading │ " " │ ( l ) or ( L ) save logs to file │ " " │ ( 5 ) or ( Page Down ) toggle mouse capture - if disabled, text on screen can be selected & copied │ " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_one_and_two_definitions.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_one_and_two_definitions.snap index 1b760e1..15f2de0 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_one_and_two_definitions.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_one_and_two_definitions.snap @@ -2,52 +2,53 @@ source: src/ui/draw_blocks/help.rs expression: setup.terminal.backend() --- -" ╭ 0.00.000 ──────────────────────────────────────────────────────────────────────────╮ " -" │ │ " -" │ 88 │ " -" │ 88 │ " -" │ 88 │ " -" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ " -" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ " -" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ " -" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ " -" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ " -" │ │ " -" │ A simple tui to view & control docker containers │ " -" │ │ " -" │ ( s ) or ( S ) select next panel │ " -" │ ( t ) select previous panel │ " -" │ ( n ) scroll list down by one │ " -" │ ( r ) scroll list up by one │ " -" │ ( m ) or ( M ) scroll list down by many │ " -" │ ( q ) or ( Q ) scroll list by up many │ " -" │ ( o ) or ( O ) scroll list to end │ " -" │ ( p ) scroll list to start │ " -" │ ( g ) or ( G ) horizontal scroll logs right │ " -" │ ( f ) horizontal scroll logs left │ " -" │ ( enter ) send docker container command │ " -" │ ( d ) exec into a container │ " -" │ ( 4 ) or ( 5 ) toggle this help information - or click heading │ " -" │ ( l ) save logs to file │ " -" │ ( 5 ) toggle mouse capture - if disabled, text on screen can be selected & copied │ " -" │ ( e ) or ( E ) enter filter mode │ " -" │ ( 3 ) reset container sorting │ " -" │ ( y ) or ( Y ) sort containers by name │ " -" │ ( 0 ) or ( 9 ) sort containers by state │ " -" │ ( 1 ) sort containers by status │ " -" │ ( u ) or ( U ) sort containers by cpu │ " -" │ ( x ) sort containers by memory │ " -" │ ( v ) sort containers by id │ " -" │ ( w ) or ( W ) sort containers by image │ " -" │ ( z ) sort containers by rx │ " -" │ ( 2 ) or ( 7 ) sort containers by tx │ " -" │ ( h ) decrease log section height │ " -" │ ( i ) or ( I ) increase log section height │ " -" │ ( j ) toggle log section visibility │ " -" │ ( a ) or ( A ) close dialog │ " -" │ ( k ) or ( K ) quit at any time │ " -" │ │ " -" │ currently an early work in progress, all and any input appreciated │ " -" │ https://github.com/mrjackwills/oxker │ " -" │ │ " -" ╰────────────────────────────────────────────────────────────────────────────────────╯ " +" ╭ 0.00.000 ──────────────────────────────────────────────────────────────────────────────────╮ " +" │ │ " +" │ 88 │ " +" │ 88 │ " +" │ 88 │ " +" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ " +" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ " +" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ " +" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ " +" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ " +" │ │ " +" │ A simple tui to view & control docker containers │ " +" │ │ " +" │ ( t ) select next panel │ " +" │ ( u ) or ( U ) select previous panel │ " +" │ ( o ) or ( O ) scroll list down by one │ " +" │ ( s ) or ( S ) scroll list up by one │ " +" │ ( n ) scroll list down by many │ " +" │ ( r ) scroll list by up many │ " +" │ ( p ) scroll list to end │ " +" │ ( q ) or ( Q ) scroll list to start │ " +" │ ( h ) horizontal scroll logs right │ " +" │ ( g ) or ( G ) horizontal scroll logs left │ " +" │ ( enter ) send docker container command │ " +" │ ( d ) exec into a container │ " +" │ ( f ) force clear the screen & redraw the gui │ " +" │ ( 5 ) toggle this help information - or click heading │ " +" │ ( m ) or ( M ) save logs to file │ " +" │ ( 6 ) or ( 7 ) toggle mouse capture - if disabled, text on screen can be selected & copied │ " +" │ ( e ) or ( E ) enter filter mode │ " +" │ ( 4 ) or ( 5 ) reset container sorting │ " +" │ ( z ) sort containers by name │ " +" │ ( 1 ) sort containers by state │ " +" │ ( 2 ) or ( 7 ) sort containers by status │ " +" │ ( v ) sort containers by cpu │ " +" │ ( y ) or ( Y ) sort containers by memory │ " +" │ ( w ) or ( W ) sort containers by id │ " +" │ ( x ) sort containers by image │ " +" │ ( 0 ) or ( 9 ) sort containers by rx │ " +" │ ( 3 ) sort containers by tx │ " +" │ ( i ) or ( I ) decrease log section height │ " +" │ ( j ) increase log section height │ " +" │ ( k ) or ( K ) toggle log section visibility │ " +" │ ( a ) or ( A ) close dialog │ " +" │ ( l ) quit at any time │ " +" │ │ " +" │ currently an early work in progress, all and any input appreciated │ " +" │ https://github.com/mrjackwills/oxker │ " +" │ │ " +" ╰────────────────────────────────────────────────────────────────────────────────────────────╯ " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_show_timezone.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_show_timezone.snap index ceed130..82193cb 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_show_timezone.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_show_timezone.snap @@ -2,7 +2,6 @@ source: src/ui/draw_blocks/help.rs expression: setup.terminal.backend() --- -" " " ╭ 0.00.000 ─────────────────────────────────────────────────────────────────────────╮ " " │ │ " " │ 88 │ " @@ -23,6 +22,7 @@ expression: setup.terminal.backend() " │ ( ← → ) horizontal scroll across logs │ " " │ ( enter ) send docker container command │ " " │ ( e ) exec into a container │ " +" │ ( f ) force clear the screen & redraw the gui │ " " │ ( h ) toggle this help information - or click heading │ " " │ ( s ) save logs to file │ " " │ ( m ) toggle mouse capture - if disabled, text on screen can be selected & copied │ " @@ -37,5 +37,5 @@ expression: setup.terminal.backend() " │ currently an early work in progress, all and any input appreciated │ " " │ https://github.com/mrjackwills/oxker │ " " │ │ " +" │ │ " " ╰───────────────────────────────────────────────────────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_help_panel.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_help_panel.snap index ad38e21..65aed11 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_help_panel.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_help_panel.snap @@ -23,6 +23,7 @@ expression: setup.terminal.backend() "│ │ ( ← → ) horizontal scroll across logs │ │" "│ │ ( enter ) send docker container command │ │" "│ │ ( e ) exec into a container │ │" +"│ │ ( f ) force clear the screen & redraw the gui │ │" "│ │ ( h ) toggle this help information - or click heading │ │" "│ │ ( s ) save logs to file │ │" "│ │ ( m ) toggle mouse capture - if disabled, text on screen can be selected & copied │ │" @@ -30,13 +31,12 @@ expression: setup.terminal.backend() "│ │ ( 0 ) stop sort │ │" "│ │ ( 1 - 9 ) sort by header - or click header │ │" "│ │ ( - = ) change log section height │ │" -"│ │ ( \ ) toggle log section visibility │ │" -"╰────────────────────────────────────│ ( esc ) close dialog │────────────────────────────────────╯" -"╭───────────────────────── cpu 03.00%│ ( q ) quit at any time │──────╮╭────────── ports ───────────╮" -"│10.00%│ •• │ │ ││ ip private public│" -"│ │ • • │ currently an early work in progress, all and any input appreciated │ ││ 8001 │" -"│ │ •• • │ https://github.com/mrjackwills/oxker │ ││127.0.0.1 8003 8003│" -"│ │ • • │ │ ││ │" +"╰────────────────────────────────────│ ( \ ) toggle log section visibility │────────────────────────────────────╯" +"╭───────────────────────── cpu 03.00%│ ( esc ) close dialog │──────╮╭────────── ports ───────────╮" +"│10.00%│ •• │ ( q ) quit at any time │ ││ ip private public│" +"│ │ • • │ │ ││ 8001 │" +"│ │ •• • │ currently an early work in progress, all and any input appreciated │ ││127.0.0.1 8003 8003│" +"│ │ • • │ https://github.com/mrjackwills/oxker │ ││ │" "│ │ •• • • │ │ ││ │" "│ │• •• ╰────────────────────────────────────────────────────────────────────────────────────╯ ││ │" "│ │• • ││ │• • ││ │" diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs index 5915363..dd0aac9 100644 --- a/src/ui/gui_state.rs +++ b/src/ui/gui_state.rs @@ -217,7 +217,7 @@ impl GuiState { pub fn log_height_increase(&mut self) { if self.show_logs && self.log_height <= 75 { self.log_height = self.log_height.saturating_add(5); - self.rerender.update(); + self.rerender.update_draw(); } } @@ -230,7 +230,7 @@ impl GuiState { self.show_logs = false; self.selected_panel = SelectablePanel::Containers; } - self.rerender.update(); + self.rerender.update_draw(); } } @@ -253,7 +253,7 @@ impl GuiState { if !self.show_logs && self.selected_panel == SelectablePanel::Logs { self.selected_panel = SelectablePanel::Containers; } - self.rerender.update(); + self.rerender.update_draw(); } /// Set the log_height to zero, for now only used by tests @@ -272,6 +272,11 @@ impl GuiState { self.intersect_panel.clear(); } + /// Set the rerender clear to true, to flush the screen and redraw + pub fn set_clear(&self) { + self.rerender.set_clear(); + } + /// Get the currently selected panel pub const fn get_selected_panel(&self) -> SelectablePanel { self.selected_panel @@ -287,7 +292,7 @@ impl GuiState { .first() { self.selected_panel = *data.0; - self.rerender.update(); + self.rerender.update_draw(); } } @@ -360,7 +365,7 @@ impl GuiState { self.status_del(Status::DeleteConfirm); } self.delete_container_id = id; - self.rerender.update(); + self.rerender.update_draw(); } /// Return a copy of the Status HashSet @@ -381,7 +386,7 @@ impl GuiState { } _ => (), } - self.rerender.update(); + self.rerender.update_draw(); } /// Inset the ExecMode into self, and set the Status as exec @@ -390,7 +395,7 @@ impl GuiState { pub fn set_exec_mode(&mut self, mode: ExecMode) { self.exec_mode = Some(mode); self.status.insert(Status::Exec); - self.rerender.update(); + self.rerender.update_draw(); } pub fn get_exec_mode(&self) -> Option { @@ -402,7 +407,7 @@ impl GuiState { pub fn status_push(&mut self, status: Status) { if status != Status::Exec { self.status.insert(status); - self.rerender.update(); + self.rerender.update_draw(); } } @@ -415,7 +420,7 @@ impl GuiState { { self.selected_panel = self.selected_panel.next(); } - self.rerender.update(); + self.rerender.update_draw(); } /// Change to previous selectable panel @@ -427,7 +432,7 @@ impl GuiState { { self.selected_panel = self.selected_panel.prev(); } - self.rerender.update(); + self.rerender.update_draw(); } /// Insert a new loading_uuid into HashSet, and advance the loading_index by one frame, or reset to 0 if at end of array @@ -438,7 +443,7 @@ impl GuiState { self.loading_index += 1; } self.loading_set.insert(uuid); - self.rerender.update(); + self.rerender.update_draw(); } pub fn is_loading(&self) -> bool { @@ -471,7 +476,7 @@ impl GuiState { /// Stop the loading_spin function, and reset gui loading status pub fn stop_loading_animation(&mut self, loading_uuid: Uuid) { self.loading_set.remove(&loading_uuid); - self.rerender.update(); + self.rerender.update_draw(); if self.loading_set.is_empty() { self.loading_index = 0; if let Some(h) = &self.loading_handle { @@ -484,12 +489,12 @@ impl GuiState { /// Set info box content pub fn set_info_box(&mut self, text: &str) { self.info_box_text = Some((text.to_owned(), std::time::Instant::now())); - self.rerender.update(); + self.rerender.update_draw(); } /// Remove info box content pub fn reset_info_box(&mut self) { self.info_box_text = None; - self.rerender.update(); + self.rerender.update_draw(); } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 412a6be..b749aed 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -50,7 +50,7 @@ pub struct Ui { input_tx: Sender, is_running: Arc, now: Instant, - redraw: Arc, + rerender: Arc, terminal: Terminal>, } @@ -73,7 +73,7 @@ impl Ui { gui_state: Arc>, input_tx: Sender, is_running: Arc, - redraw: Arc, + rerender: Arc, ) { match Self::setup_terminal() { Ok(mut terminal) => { @@ -85,7 +85,7 @@ impl Ui { input_tx, is_running, now: Instant::now(), - redraw, + rerender, terminal, }; if let Err(e) = ui.draw_ui().await { @@ -169,6 +169,13 @@ impl Ui { Ok(()) } + /// Check if the user has attempt to clear the screen, and if so clear and redraw + fn check_clear(&mut self) { + if self.rerender.get_clear() { + self.terminal.clear().ok(); + self.rerender.update_draw(); + } + } /// Use external docker cli to exec into a container async fn exec(&mut self) { let exec_mode = self.gui_state.lock().get_exec_mode(); @@ -191,7 +198,8 @@ impl Ui { /// Use the previously redrawn time, the current time, the docker_interval, and the redraw struct, to calculate /// if the screen should be redrawn or not fn should_redraw(&self, previous: &mut Instant, docker_interval_ms: u128) -> bool { - let result = self.redraw.swap() || previous.elapsed().as_millis() >= docker_interval_ms; + let result = + self.rerender.swap_draw() || previous.elapsed().as_millis() >= docker_interval_ms; if result { *previous = std::time::Instant::now(); } @@ -210,6 +218,10 @@ impl Ui { } while self.is_running.load(Ordering::SeqCst) { + // if self.redraw.get_clear() { + // self.terminal.clear().ok(); + // continue; + // } if self.should_redraw(&mut drawn_at, docker_interval_ms) { let fd = FrameData::from(&*self); @@ -249,15 +261,12 @@ impl Ui { } } else if let Event::Resize(width, _) = event { self.gui_state.lock().clear_area_map(); - - // self.gui_state.lock().set_window_height(row); - self.terminal.autoresize().ok(); - // todo set screen width self.gui_state.lock().set_screen_width(width); } } } + self.check_clear(); } Ok(()) } diff --git a/src/ui/redraw.rs b/src/ui/redraw.rs index 4fd40d2..0647853 100644 --- a/src/ui/redraw.rs +++ b/src/ui/redraw.rs @@ -1,21 +1,40 @@ use std::sync::atomic::{AtomicBool, Ordering}; #[derive(Debug)] -pub struct Rerender(AtomicBool); +pub struct Rerender { + draw: AtomicBool, + clear: AtomicBool, +} impl Rerender { pub const fn new() -> Self { - Self(AtomicBool::new(true)) + Self { + draw: AtomicBool::new(true), + clear: AtomicBool::new(false), + } } - pub fn update(&self) { - self.0.store(true, Ordering::SeqCst); + pub fn update_draw(&self) { + self.draw.store(true, Ordering::SeqCst); } - /// Return the value of the self, and set to false - pub fn swap(&self) -> bool { + pub fn get_clear(&self) -> bool { + if self.clear.load(Ordering::SeqCst) { + self.clear.store(false, Ordering::SeqCst); + true + } else { + false + } + } + + pub fn set_clear(&self) { + self.clear.store(true, Ordering::SeqCst); + } + + /// Return the value of the draw, and set to false + pub fn swap_draw(&self) -> bool { match self - .0 + .draw .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst) { Ok(previous_value) => previous_value, From ced885e0128b6d5d3a3c7cb97d7e53bc2da64893 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Fri, 15 Aug 2025 09:05:27 +0000 Subject: [PATCH 06/20] chore: dependencies updated --- Cargo.lock | 269 ++++++++++---------- Cargo.toml | 8 +- src/app_data/container_state.rs | 119 ++++++--- src/app_data/mod.rs | 7 +- src/docker_data/mod.rs | 426 ++++++++++++++------------------ src/input_handler/mod.rs | 5 +- src/main.rs | 6 +- 7 files changed, 424 insertions(+), 416 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc3647a..4a00396 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -70,35 +70,35 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" @@ -129,9 +129,9 @@ checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "bollard" -version = "0.18.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ccca1260af6a459d75994ad5acc1651bcabcbdbc41467cc9786519ab854c30" +checksum = "8796b390a5b4c86f9f2e8173a68c2791f4fa6b038b84e96dbc01c016d1e6722c" dependencies = [ "base64", "bollard-stubs", @@ -162,20 +162,21 @@ dependencies = [ [[package]] name = "bollard-stubs" -version = "1.47.1-rc.27.3.1" +version = "1.49.0-rc.28.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f179cfbddb6e77a5472703d4b30436bff32929c0aa8a9008ecf23d1d3cdd0da" +checksum = "2e7814991259013d5a5bee4ae28657dae0747d843cf06c40f7fc0c2894d6fa38" dependencies = [ "serde", + "serde_json", "serde_repr", "serde_with", ] [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" @@ -197,18 +198,18 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "castaway" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" dependencies = [ "rustversion", ] [[package]] name = "cc" -version = "1.2.27" +version = "1.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" dependencies = [ "shlex", ] @@ -234,9 +235,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.40" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" dependencies = [ "clap_builder", "clap_derive", @@ -244,9 +245,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.40" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ "anstream", "anstyle", @@ -258,9 +259,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.40" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" dependencies = [ "heck", "proc-macro2", @@ -349,7 +350,7 @@ dependencies = [ "document-features", "mio", "parking_lot", - "rustix 1.0.7", + "rustix 1.0.8", "signal-hook", "signal-hook-mio", "winapi", @@ -473,9 +474,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" @@ -497,12 +498,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -615,9 +616,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -719,9 +720,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ "bytes", "futures-channel", @@ -903,12 +904,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "serde", ] @@ -931,9 +932,9 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" +checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" dependencies = [ "darling", "indoc", @@ -942,6 +943,17 @@ dependencies = [ "syn", ] +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1023,15 +1035,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ "bitflags", "libc", @@ -1057,9 +1069,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "litrs" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" [[package]] name = "lock_api" @@ -1083,7 +1095,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.4", + "hashbrown 0.15.5", ] [[package]] @@ -1285,9 +1297,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" dependencies = [ "unicode-ident", ] @@ -1309,9 +1321,9 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", "rand_core", @@ -1359,18 +1371,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] [[package]] name = "redox_users" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", @@ -1399,9 +1411,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustix" @@ -1418,22 +1430,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -1453,6 +1465,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1481,9 +1505,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ "itoa", "memchr", @@ -1515,9 +1539,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.9" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" dependencies = [ "serde", ] @@ -1536,16 +1560,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" dependencies = [ "base64", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.9.0", - "schemars", + "indexmap 2.10.0", + "schemars 0.9.0", + "schemars 1.0.4", "serde", "serde_derive", "serde_json", @@ -1590,9 +1615,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -1605,9 +1630,9 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -1617,12 +1642,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.10" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1667,9 +1692,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.103" +version = "2.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" dependencies = [ "proc-macro2", "quote", @@ -1689,18 +1714,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" dependencies = [ "proc-macro2", "quote", @@ -1759,20 +1784,22 @@ dependencies = [ [[package]] name = "tokio" -version = "1.45.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", + "slab", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1788,9 +1815,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -1801,35 +1828,32 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_parser", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" dependencies = [ "serde", ] [[package]] -name = "toml_edit" -version = "0.22.27" +name = "toml_parser" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" dependencies = [ - "indexmap 2.9.0", - "serde", - "serde_spanned", - "toml_datetime", "winnow", ] @@ -1968,9 +1992,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" dependencies = [ "getrandom 0.3.3", "js-sys", @@ -2147,15 +2171,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.59.0" @@ -2171,7 +2186,7 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.3", ] [[package]] @@ -2192,10 +2207,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -2304,12 +2320,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" -dependencies = [ - "memchr", -] +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" [[package]] name = "wit-bindgen-rt" @@ -2352,18 +2365,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", @@ -2404,9 +2417,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", diff --git a/Cargo.toml b/Cargo.toml index 3d0ea62..5e72211 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ similar_names = "allow" [dependencies] anyhow = "1.0" -bollard = "0.18" +bollard = "0.19" cansi = "2.2" clap = { version = "4.5", features = ["color", "derive", "unicode"] } crossterm = "0.29" @@ -39,12 +39,12 @@ ratatui = "0.29" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_jsonc = "1.0" -tokio = { version = "1.45", features = ["full"] } +tokio = { version = "1.47", features = ["full"] } tokio-util = "0.7" -toml = { version = "0.8", default-features = false, features = ["parse"] } +toml = { version = "0.9", default-features = false, features = ["parse", "serde"] } tracing = "0.1" tracing-subscriber = "0.3" -uuid = { version = "1.17", features = ["fast-rng", "v4"] } +uuid = { version = "1.18", features = ["fast-rng", "v4"] } [profile.release] lto = true diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index ed605f0..9462796 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -7,7 +7,12 @@ use std::{ use bollard::service::Port; use jiff::{Timestamp, tz::TimeZone}; -use ratatui::{layout::Size, style::Color, text::Text, widgets::ListState}; +use ratatui::{ + layout::Size, + style::Color, + text::{Line, Text}, + widgets::ListState, +}; use crate::config::AppColors; @@ -323,6 +328,54 @@ impl From<(&str, &ContainerStatus)> for State { } } +/// Need status, to check if container is unhealthy or not +impl + From<( + &bollard::secret::ContainerSummaryStateEnum, + &ContainerStatus, + )> for State +{ + fn from( + (input, status): ( + &bollard::secret::ContainerSummaryStateEnum, + &ContainerStatus, + ), + ) -> Self { + match input { + bollard::secret::ContainerSummaryStateEnum::DEAD => Self::Dead, + bollard::secret::ContainerSummaryStateEnum::EXITED => Self::Exited, + bollard::secret::ContainerSummaryStateEnum::PAUSED => Self::Paused, + bollard::secret::ContainerSummaryStateEnum::REMOVING => Self::Removing, + bollard::secret::ContainerSummaryStateEnum::RESTARTING => Self::Restarting, + bollard::secret::ContainerSummaryStateEnum::RUNNING => { + if status.unhealthy() { + Self::Running(RunningState::Unhealthy) + } else { + Self::Running(RunningState::Healthy) + } + } + _ => Self::Unknown, + } + } +} + +/// Again, need status, to check if container is unhealthy or not +impl + From<( + Option<&bollard::secret::ContainerSummaryStateEnum>, + &ContainerStatus, + )> for State +{ + fn from( + (input, status): ( + Option<&bollard::secret::ContainerSummaryStateEnum>, + &ContainerStatus, + ), + ) -> Self { + input.map_or(Self::Unknown, |input| Self::from((input, status))) + } +} + /// Again, need status, to check if container is unhealthy or not impl From<(Option, &ContainerStatus)> for State { fn from((input, status): (Option, &ContainerStatus)) -> Self { @@ -590,45 +643,41 @@ impl Logs { /// `text` *should* only be a single line, so just use the .first() method rather than trying to iterate fn format_log_line(text: &Text<'static>, char_offset: usize, width: u16) -> Text<'static> { let mut skipped = 0; - Text::from( - text.lines - .first() - .map(|line| { - ratatui::text::Line::from( - line.spans - .iter() - .filter_map(|span| { - if skipped >= char_offset { - return Some(ratatui::text::Span::styled( - span.content.chars().take(width.into()).collect::(), - span.style, - )); - } - let span_len = span.content.chars().count(); - if skipped + span_len <= char_offset { - skipped += span_len; - None - } else { - let start_index = char_offset - skipped; - skipped = char_offset; - let new_content = span - .content + text.lines.first().map_or_else(Text::default, |line| { + Text::from(Line::from( + line.spans + .iter() + .filter_map(|span| { + if skipped >= char_offset { + Some(ratatui::text::Span::styled( + span.content.chars().take(width.into()).collect::(), + span.style, + )) + } else { + let span_len = span.content.chars().count(); + if skipped + span_len <= char_offset { + skipped += span_len; + None + } else { + let start_index = char_offset - skipped; + skipped = char_offset; + Some(ratatui::text::Span::styled( + span.content .chars() .skip(start_index) .take(width.into()) - .collect::(); - Some(ratatui::text::Span::styled(new_content, span.style)) - } - }) - .collect::>(), - ) - }) - .into_iter() - .collect::>(), - ) + .collect::(), + span.style, + )) + } + } + }) + .collect::>(), + )) + }) } - /// Get the logs vec, but instead of cloning to whole vec, only clone items within x of the currently selected index, as ell as only the current screen widths number of chars + /// Get the logs vec, but instead of cloning to whole vec, only clone items within x of the currently selected index, as well as only the current screen widths number of chars /// Where x is the abs different of the index plus the panel height & a padding /// Take into account the char offset, so that can scroll a line /// The rest can be just empty list items diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index f8508ad..83c1415 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -896,7 +896,12 @@ impl AppData { .as_ref() .map_or(String::new(), std::clone::Clone::clone), ); - let state = State::from((i.state.as_ref().map_or("dead", |z| z), &status)); + let state = State::from(( + i.state + .as_ref() + .map_or(&bollard::secret::ContainerSummaryStateEnum::DEAD, |z| z), + &status, + )); let image = i .image .as_ref() diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index e8c0237..7aae791 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -1,9 +1,10 @@ use bollard::{ Docker, - container::{ - ListContainersOptions, LogsOptions, MemoryStatsStats, RemoveContainerOptions, - StartContainerOptions, Stats, StatsOptions, + query_parameters::{ + ListContainersOptions, LogsOptions, RemoveContainerOptions, RestartContainerOptions, + StartContainerOptions, StatsOptions, StopContainerOptions, }, + secret::ContainerStatsResponse, service::ContainerSummary, }; use futures_util::StreamExt; @@ -75,31 +76,44 @@ pub struct DockerData { impl DockerData { /// Use docker stats to calculate current cpu usage #[allow(clippy::cast_precision_loss)] - fn calculate_usage(stats: &Stats) -> f64 { + fn calculate_usage(stats: &ContainerStatsResponse) -> f64 { let mut cpu_percentage = 0.0; - let cpu_delta = stats - .cpu_stats - .cpu_usage - .total_usage - .saturating_sub(stats.precpu_stats.cpu_usage.total_usage) - as f64; - if let (Some(cpu_stats_usage), Some(precpu_stats_usage)) = ( - stats.cpu_stats.system_cpu_usage, - stats.precpu_stats.system_cpu_usage, + let total_usage = stats.precpu_stats.as_ref().map_or(0, |i| { + i.cpu_usage + .as_ref() + .map_or(0, |i| i.total_usage.unwrap_or_default()) + }); + + let cpu_delta = stats.cpu_stats.as_ref().map_or(0, |i| { + i.cpu_usage.as_ref().map_or(0, |i| { + i.total_usage + .unwrap_or_default() + .saturating_sub(total_usage) + }) + }) as f64; + + if let (Some(Some(cpu_stats_usage)), Some(Some(precpu_stats_usage))) = ( + stats.cpu_stats.as_ref().map(|i| i.system_cpu_usage), + stats.precpu_stats.as_ref().map(|i| i.system_cpu_usage), ) { let system_delta = cpu_stats_usage.saturating_sub(precpu_stats_usage) as f64; - let online_cpus = stats.cpu_stats.online_cpus.unwrap_or_else(|| { - u64::try_from( - stats - .cpu_stats - .cpu_usage - .percpu_usage - .as_ref() - .map_or(0, std::vec::Vec::len), - ) - .unwrap_or_default() - }) as f64; + let online_cpus = f64::from(stats.cpu_stats.as_ref().map_or(0, |i| { + i.online_cpus.unwrap_or_else(|| { + u32::try_from( + stats + .cpu_stats + .clone() + .unwrap_or_default() + .cpu_usage + .unwrap_or_default() + .percpu_usage + .as_ref() + .map_or(0, std::vec::Vec::len), + ) + .unwrap_or_default() + }) + })); if system_delta > 0.0 && cpu_delta > 0.0 { cpu_percentage = (cpu_delta / system_delta) * online_cpus * 100.0; } @@ -131,20 +145,23 @@ impl DockerData { ) .take(1); + // some err here while let Some(Ok(stats)) = stream.next().await { // Memory stats are only collected if the container is alive - is this the behaviour we want? + let (mem_stat, cpu_stats) = if state.is_alive() { - let mem_cache = stats.memory_stats.stats.map_or(0, |i| match i { - MemoryStatsStats::V1(x) => x.inactive_file, - MemoryStatsStats::V2(x) => x.inactive_file, + let mem_cache = stats.memory_stats.as_ref().map_or(&0, |i| { + i.stats + .as_ref() + .map_or(&0, |i| i.get("inactive_file").unwrap_or(&0)) }); ( Some( stats .memory_stats - .usage - .unwrap_or_default() - .saturating_sub(mem_cache), + .as_ref() + .map_or(0, |i| i.usage.unwrap_or_default()) + .saturating_sub(*mem_cache), ), Some(Self::calculate_usage(&stats)), ) @@ -152,26 +169,25 @@ impl DockerData { (None, None) }; - let op_key = stats - .networks - .as_ref() - .and_then(|networks| networks.keys().next().cloned()); - - let (rx, tx) = if let Some(key) = op_key { - stats - .networks - .unwrap_or_default() - .get(&key) - .map_or((0, 0), |f| (f.rx_bytes, f.tx_bytes)) - } else { - (0, 0) - }; + // TODO Is hardcoded eth0 a good idea here? + let (rx, tx) = stats.networks.as_ref().map_or((0, 0), |i| { + i.get("eth0").map_or((0, 0), |x| { + ( + x.rx_bytes.unwrap_or_default(), + x.tx_bytes.unwrap_or_default(), + ) + }) + }); app_data.lock().update_stats_by_id( id, cpu_stats, mem_stat, - stats.memory_stats.limit.unwrap_or_default(), + stats + .memory_stats + .unwrap_or_default() + .limit + .unwrap_or_default(), rx, tx, ); @@ -206,7 +222,7 @@ impl DockerData { async fn update_all_containers(&self) { let containers = self .docker - .list_containers(Some(ListContainersOptions:: { + .list_containers(Some(ListContainersOptions { all: true, ..Default::default() })) @@ -244,11 +260,11 @@ impl DockerData { spawns: Arc>>>, stderr: bool, ) { - let options = Some(LogsOptions:: { + let options = Some(LogsOptions { stdout: true, stderr, timestamps: true, - since: i64::try_from(since).unwrap_or_default(), + since: i32::try_from(since).unwrap_or_default(), ..Default::default() }); @@ -365,14 +381,22 @@ impl DockerData { .await } DockerCommand::Pause => docker.pause_container(id.get()).await, - DockerCommand::Restart => docker.restart_container(id.get(), None).await, + DockerCommand::Restart => { + docker + .restart_container(id.get(), None::) + .await + } DockerCommand::Resume => docker.unpause_container(id.get()).await, DockerCommand::Start => { docker - .start_container(id.get(), None::>) + .start_container(id.get(), None::) + .await + } + DockerCommand::Stop => { + docker + .stop_container(id.get(), None::) .await } - DockerCommand::Stop => docker.stop_container(id.get(), None).await, } .is_err() { @@ -448,119 +472,72 @@ impl DockerData { #[allow(clippy::float_cmp)] mod tests { - use bollard::container::{ - BlkioStats, CPUStats, CPUUsage, MemoryStats, PidsStats, Stats, StorageStats, ThrottlingData, - }; + use bollard::secret::{ContainerCpuStats, ContainerCpuUsage}; use super::*; - fn gen_stats() -> Stats { - Stats { - read: String::new(), - preread: String::new(), - num_procs: 1, - pids_stats: PidsStats { - current: None, - limit: None, - }, - network: None, + fn gen_stats() -> ContainerStatsResponse { + ContainerStatsResponse { + read: None, + preread: None, + num_procs: Some(1), + pids_stats: None, networks: None, - memory_stats: MemoryStats { - stats: None, - max_usage: None, - usage: None, - failcnt: None, - limit: None, - commit: None, - commit_peak: None, - commitbytes: None, - commitpeakbytes: None, - privateworkingset: None, - }, - blkio_stats: BlkioStats { - io_service_bytes_recursive: None, - io_serviced_recursive: None, - io_queue_recursive: None, - io_service_time_recursive: None, - io_wait_time_recursive: None, - io_merged_recursive: None, - io_time_recursive: None, - sectors_recursive: None, - }, - cpu_stats: CPUStats { - cpu_usage: CPUUsage { + memory_stats: None, + blkio_stats: None, + cpu_stats: Some(ContainerCpuStats { + cpu_usage: Some(ContainerCpuUsage { percpu_usage: Some(vec![50]), - usage_in_usermode: 10, - total_usage: 100, - usage_in_kernelmode: 20, - }, + usage_in_usermode: Some(10), + total_usage: Some(100), + usage_in_kernelmode: Some(20), + }), system_cpu_usage: Some(400), online_cpus: Some(1), - throttling_data: ThrottlingData { - periods: 0, - throttled_periods: 0, - throttled_time: 0, - }, - }, - precpu_stats: CPUStats { - cpu_usage: CPUUsage { + throttling_data: None, + }), + precpu_stats: Some(ContainerCpuStats { + cpu_usage: Some(ContainerCpuUsage { percpu_usage: Some(vec![50]), - usage_in_usermode: 10, - total_usage: 100, - usage_in_kernelmode: 20, - }, + usage_in_usermode: Some(10), + total_usage: Some(100), + usage_in_kernelmode: Some(20), + }), system_cpu_usage: Some(400), online_cpus: Some(1), - throttling_data: ThrottlingData { - periods: 0, - throttled_periods: 0, - throttled_time: 0, - }, - }, - storage_stats: StorageStats { - read_count_normalized: None, - read_size_bytes: None, - write_count_normalized: None, - write_size_bytes: None, - }, - name: String::new(), - id: String::new(), + throttling_data: None, + }), + storage_stats: None, + name: None, + id: None, } } #[test] fn test_calculate_usage_50() { let mut stats = gen_stats(); - stats.precpu_stats = CPUStats { - cpu_usage: CPUUsage { + stats.precpu_stats = Some(ContainerCpuStats { + cpu_usage: Some(ContainerCpuUsage { percpu_usage: Some(vec![50]), - usage_in_usermode: 10, - total_usage: 100, - usage_in_kernelmode: 20, - }, + usage_in_usermode: Some(10), + total_usage: Some(100), + usage_in_kernelmode: Some(20), + }), system_cpu_usage: Some(400), online_cpus: Some(1), - throttling_data: ThrottlingData { - periods: 0, - throttled_periods: 0, - throttled_time: 0, - }, - }; - stats.cpu_stats = CPUStats { - cpu_usage: CPUUsage { + throttling_data: None, + }); + stats.cpu_stats = Some(ContainerCpuStats { + cpu_usage: Some(ContainerCpuUsage { percpu_usage: Some(vec![150]), - usage_in_usermode: 20, - total_usage: 150, - usage_in_kernelmode: 30, - }, + usage_in_usermode: Some(20), + total_usage: Some(150), + usage_in_kernelmode: Some(30), + }), system_cpu_usage: Some(500), online_cpus: Some(1), - throttling_data: ThrottlingData { - periods: 0, - throttled_periods: 0, - throttled_time: 0, - }, - }; + throttling_data: None, + }); let cpu_percentage = DockerData::calculate_usage(&stats); assert_eq!(50.0, cpu_percentage); } @@ -568,37 +545,28 @@ mod tests { #[test] fn test_calculate_usage_25() { let mut stats = gen_stats(); - stats.precpu_stats = CPUStats { - cpu_usage: CPUUsage { + stats.precpu_stats = Some(ContainerCpuStats { + cpu_usage: Some(ContainerCpuUsage { percpu_usage: Some(vec![50]), - usage_in_usermode: 10, - total_usage: 100, - usage_in_kernelmode: 20, - }, + usage_in_usermode: Some(10), + total_usage: Some(100), + usage_in_kernelmode: Some(20), + }), system_cpu_usage: Some(400), online_cpus: Some(1), - throttling_data: ThrottlingData { - periods: 0, - throttled_periods: 0, - throttled_time: 0, - }, - }; - stats.cpu_stats = CPUStats { - cpu_usage: CPUUsage { + throttling_data: None, + }); + stats.cpu_stats = Some(ContainerCpuStats { + cpu_usage: Some(ContainerCpuUsage { percpu_usage: Some(vec![75]), - usage_in_usermode: 20, - total_usage: 125, - usage_in_kernelmode: 30, - }, + usage_in_usermode: Some(20), + total_usage: Some(125), + usage_in_kernelmode: Some(30), + }), system_cpu_usage: Some(500), online_cpus: Some(1), - throttling_data: ThrottlingData { - periods: 0, - throttled_periods: 0, - throttled_time: 0, - }, - }; - + throttling_data: None, + }); let cpu_percentage = DockerData::calculate_usage(&stats); assert_eq!(25.0, cpu_percentage); } @@ -606,38 +574,28 @@ mod tests { #[test] fn test_calculate_usage_75() { let mut stats = gen_stats(); - stats.precpu_stats = CPUStats { - cpu_usage: CPUUsage { + stats.precpu_stats = Some(ContainerCpuStats { + cpu_usage: Some(ContainerCpuUsage { percpu_usage: Some(vec![50]), - usage_in_usermode: 10, - total_usage: 100, - usage_in_kernelmode: 20, - }, + usage_in_usermode: Some(10), + total_usage: Some(100), + usage_in_kernelmode: Some(20), + }), system_cpu_usage: Some(400), online_cpus: Some(1), - throttling_data: ThrottlingData { - periods: 0, - throttled_periods: 0, - throttled_time: 0, - }, - }; - - stats.cpu_stats = CPUStats { - cpu_usage: CPUUsage { + throttling_data: None, + }); + stats.cpu_stats = Some(ContainerCpuStats { + cpu_usage: Some(ContainerCpuUsage { percpu_usage: Some(vec![175]), - usage_in_usermode: 20, - total_usage: 175, - usage_in_kernelmode: 30, - }, + usage_in_usermode: Some(20), + total_usage: Some(175), + usage_in_kernelmode: Some(30), + }), system_cpu_usage: Some(500), online_cpus: Some(1), - throttling_data: ThrottlingData { - periods: 0, - throttled_periods: 0, - throttled_time: 0, - }, - }; - + throttling_data: None, + }); let cpu_percentage = DockerData::calculate_usage(&stats); assert_eq!(75.0, cpu_percentage); } @@ -645,36 +603,28 @@ mod tests { #[test] fn test_calculate_usage_100() { let mut stats = gen_stats(); - stats.precpu_stats = CPUStats { - cpu_usage: CPUUsage { + stats.precpu_stats = Some(ContainerCpuStats { + cpu_usage: Some(ContainerCpuUsage { percpu_usage: Some(vec![50]), - usage_in_usermode: 10, - total_usage: 100, - usage_in_kernelmode: 20, - }, + usage_in_usermode: Some(10), + total_usage: Some(100), + usage_in_kernelmode: Some(20), + }), system_cpu_usage: Some(400), online_cpus: Some(1), - throttling_data: ThrottlingData { - periods: 0, - throttled_periods: 0, - throttled_time: 0, - }, - }; - stats.cpu_stats = CPUStats { - cpu_usage: CPUUsage { + throttling_data: None, + }); + stats.cpu_stats = Some(ContainerCpuStats { + cpu_usage: Some(ContainerCpuUsage { percpu_usage: Some(vec![200]), - usage_in_usermode: 20, - total_usage: 200, - usage_in_kernelmode: 30, - }, + usage_in_usermode: Some(20), + total_usage: Some(200), + usage_in_kernelmode: Some(30), + }), system_cpu_usage: Some(500), online_cpus: Some(1), - throttling_data: ThrottlingData { - periods: 0, - throttled_periods: 0, - throttled_time: 0, - }, - }; + throttling_data: None, + }); let cpu_percentage = DockerData::calculate_usage(&stats); assert_eq!(100.0, cpu_percentage); } @@ -682,38 +632,28 @@ mod tests { #[test] fn test_calculate_usage_175() { let mut stats = gen_stats(); - stats.precpu_stats = CPUStats { - cpu_usage: CPUUsage { + stats.precpu_stats = Some(ContainerCpuStats { + cpu_usage: Some(ContainerCpuUsage { percpu_usage: Some(vec![50]), - usage_in_usermode: 10, - total_usage: 100, - usage_in_kernelmode: 20, - }, + usage_in_usermode: Some(10), + total_usage: Some(100), + usage_in_kernelmode: Some(20), + }), system_cpu_usage: Some(400), online_cpus: Some(1), - throttling_data: ThrottlingData { - periods: 0, - throttled_periods: 0, - throttled_time: 0, - }, - }; - - stats.cpu_stats = CPUStats { - cpu_usage: CPUUsage { + throttling_data: None, + }); + stats.cpu_stats = Some(ContainerCpuStats { + cpu_usage: Some(ContainerCpuUsage { percpu_usage: Some(vec![275]), - usage_in_usermode: 20, - total_usage: 275, - usage_in_kernelmode: 30, - }, + usage_in_usermode: Some(20), + total_usage: Some(275), + usage_in_kernelmode: Some(30), + }), system_cpu_usage: Some(500), online_cpus: Some(1), - throttling_data: ThrottlingData { - periods: 0, - throttled_periods: 0, - throttled_time: 0, - }, - }; - + throttling_data: None, + }); let cpu_percentage = DockerData::calculate_usage(&stats); assert_eq!(175.0, cpu_percentage); } diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index d45b472..faa2d03 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -5,8 +5,7 @@ use std::{ time::SystemTime, }; -use bollard::container::LogsOptions; -// use bollard::container::LogsOptions; +use bollard::query_parameters::LogsOptions; use cansi::v3::categorise_text; use crossterm::{ event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind}, @@ -188,7 +187,7 @@ impl InputHandler { let path = log_path.join(format!("{name}_{now}.log")); - let options = Some(LogsOptions:: { + let options = Some(LogsOptions { stderr: true, stdout: true, timestamps: args.show_timestamp, diff --git a/src/main.rs b/src/main.rs index 665aa4b..4de8728 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ #![allow(clippy::collapsible_if)] +// Zigbuild is stuck on 1.87.0, which means Mac builds won't work when using collapsible ifs use app_data::AppData; use app_error::AppError; @@ -151,7 +152,7 @@ async fn main() { #[allow(clippy::unwrap_used)] mod tests { - use std::sync::Arc; + use std::{str::FromStr, sync::Arc}; use bollard::service::{ContainerSummary, Port}; @@ -230,6 +231,7 @@ mod tests { pub fn gen_container_summary(index: usize, state: &str) -> ContainerSummary { ContainerSummary { + image_manifest_descriptor: None, id: Some(format!("{index}")), names: Some(vec![format!("container_{}", index)]), image: Some(format!("image_{index}")), @@ -245,7 +247,7 @@ mod tests { size_rw: None, size_root_fs: None, labels: None, - state: Some(state.to_owned()), + state: Some(bollard::secret::ContainerSummaryStateEnum::from_str(state).unwrap()), status: Some(format!("Up {index} hour")), host_config: None, network_settings: None, From 696e9e087222f13e8a39b3912ca6cb3da7e1df5c Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Fri, 15 Aug 2025 11:13:00 +0000 Subject: [PATCH 07/20] refactor: redraw get_clear() use a swap --- src/docker_data/mod.rs | 3 +-- src/ui/redraw.rs | 7 +------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index 7aae791..a42e5d4 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -145,7 +145,6 @@ impl DockerData { ) .take(1); - // some err here while let Some(Ok(stats)) = stream.next().await { // Memory stats are only collected if the container is alive - is this the behaviour we want? @@ -169,7 +168,7 @@ impl DockerData { (None, None) }; - // TODO Is hardcoded eth0 a good idea here? + // TODO is hardcoded eth0 a good idea here? - Could use first() instead? let (rx, tx) = stats.networks.as_ref().map_or((0, 0), |i| { i.get("eth0").map_or((0, 0), |x| { ( diff --git a/src/ui/redraw.rs b/src/ui/redraw.rs index 0647853..7593643 100644 --- a/src/ui/redraw.rs +++ b/src/ui/redraw.rs @@ -19,12 +19,7 @@ impl Rerender { } pub fn get_clear(&self) -> bool { - if self.clear.load(Ordering::SeqCst) { - self.clear.store(false, Ordering::SeqCst); - true - } else { - false - } + self.clear.swap(false, Ordering::SeqCst) } pub fn set_clear(&self) { From 3532fdd642ae178ba13403085c2fc698f6062cb3 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Fri, 15 Aug 2025 13:01:44 +0000 Subject: [PATCH 08/20] docs: changelog --- CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d8a280..454162d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +### Chores ++ dependencies updated, [ced885e0128b6d5d3a3c7cb97d7e53bc2da64893] ++ Rust 1.89 linting, [79d19ceeb81ae60bc5562683e405d6e74e6f2578] + +### Features ++ horizontally scroll across logs. By default use `←` & `→` keys to traverse horizontally across the logs when logs panel selected. Updated `config.toml` with `log_scroll_forward` and `log_scroll_back` [c190f0206cc55b8e45b8373f9be954e828c18b3b] ++ Force clear screen & redraw of UI. By default uses `f` key, `config.toml` updated with `force_redraw` [50edbc0cc09db864835fe81a03cba8eadafe548b] + +### Refactors ++ remove macos cfg none-const functions, Zigbuild now uses Rust 1.87.0, [eb686e2c952e04da74b3e12c0bfa015ec4615e1d] + # v0.10.5 ### 2025-06-19 @@ -11,7 +22,7 @@ + .devcontainer updated, [324f8268](https://github.com/mrjackwills/oxker/commit/324f8268278081504d5357f2ed89b78ca2c25d04) + dependencies updated, [0ace9dd6](https://github.com/mrjackwills/oxker/commit/0ace9dd662144a589341779a64d7fcd8de7d9978), [a6360075](https://github.com/mrjackwills/oxker/commit/a636007547280b3b3db69374601dbece4bc21eef) + Rust 1.87.0 linting, [395b1aa7](https://github.com/mrjackwills/oxker/commit/395b1aa7e997a528e4f21e66f5f859001c1c3ec1), [67e5888e](https://github.com/mrjackwills/oxker/commit/67e5888e008cfd504c10e47f678f9351c838be99) - +back ### Docs + example config files updated, [63ab7de7](https://github.com/mrjackwills/oxker/commit/63ab7de72897de460f31181c5a42befbee2f91d3), [8fb5ac4a](https://github.com/mrjackwills/oxker/commit/8fb5ac4a945b75f3fcd118c53be1202ccbc43c59) + README.md updated, link to directories crate, closes [#65](https://github.com/mrjackwills/oxker/issues/65), [c2bfe329](https://github.com/mrjackwills/oxker/commit/c2bfe3296563daf4b7f077469f3eeff6895720b0) From 7e892af838bbccdeb5bc2459da57075f2077e8df Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Fri, 15 Aug 2025 13:02:07 +0000 Subject: [PATCH 09/20] docs: changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 454162d..2863f28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ + Rust 1.89 linting, [79d19ceeb81ae60bc5562683e405d6e74e6f2578] ### Features -+ horizontally scroll across logs. By default use `←` & `→` keys to traverse horizontally across the logs when logs panel selected. Updated `config.toml` with `log_scroll_forward` and `log_scroll_back` [c190f0206cc55b8e45b8373f9be954e828c18b3b] ++ horizontally scroll across logs. By default use `←` & `→` keys to traverse horizontally across the lines when logs panel selected. Updated `config.toml` with `log_scroll_forward` and `log_scroll_back` [c190f0206cc55b8e45b8373f9be954e828c18b3b] + Force clear screen & redraw of UI. By default uses `f` key, `config.toml` updated with `force_redraw` [50edbc0cc09db864835fe81a03cba8eadafe548b] ### Refactors From 8939ac0345326633e794cc10a981a1f3c5c07549 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Mon, 18 Aug 2025 14:02:00 +0000 Subject: [PATCH 10/20] feat: show horizontal scroll title Show a horizontal scroll title, with arrows, if available --- src/app_data/container_state.rs | 79 ++++++++++++++++++++++++++++----- src/app_data/mod.rs | 6 +-- src/docker_data/mod.rs | 2 +- src/ui/draw_blocks/logs.rs | 1 - src/ui/draw_blocks/mod.rs | 5 ++- src/ui/mod.rs | 4 +- 6 files changed, 78 insertions(+), 19 deletions(-) diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index 9462796..d5a4ff6 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -201,6 +201,7 @@ impl StatefulList { } /// Return the current status of the select list, e.g. 2/5, + /// MAYBE add up down arrows, check if at start or end etc pub fn get_state_title(&self) -> String { if self.items.is_empty() { String::new() @@ -597,13 +598,12 @@ impl LogsTz { /// stateful list dependent on whether the timestamp is in the HashSet or not #[derive(Debug, Clone, PartialEq, Eq)] pub struct Logs { - // should just be list of spans? lines: StatefulList>, tz: HashSet, - // could probably be a u16 offset: u16, max_log_len: usize, adjusted_max_width: usize, + adjust_max_width_text_len: usize, } impl Default for Logs { @@ -615,6 +615,7 @@ impl Default for Logs { tz: HashSet::new(), offset: 0, adjusted_max_width: 0, + adjust_max_width_text_len: 0, max_log_len: 0, } } @@ -629,10 +630,24 @@ impl Logs { } } + // TODO test me! /// If scrolling horiztonally along the logs, display a counter of the position in the in the scroll, `x/y` - pub fn get_scroll_title(&self) -> Option { - if self.offset > 0 { - Some(format!(" {}/{} ", self.offset, self.adjusted_max_width)) + pub fn get_scroll_title(&mut self, width: u16) -> Option { + if self.horizontal_scroll_able(width) { + let text_width = self.adjust_max_width_text_len; + let arrow_left = if self.offset > 0 { " ←" } else { " " }; + let arrow_right = if usize::from(self.offset) < self.adjusted_max_width { + "→ " + } else { + " " + }; + Some(format!( + "{left} {offset:>text_width$}/{adjusted_max_width} {right}", + offset = self.offset, + adjusted_max_width = self.adjusted_max_width, + left = arrow_left, + right = arrow_right, + )) } else { None } @@ -709,13 +724,23 @@ impl Logs { self.lines.get_state_title() } + /// Return true it currently selected cotnainer logs are wide enough to horizontally scroll + pub fn horizontal_scroll_able(&mut self, width: u16) -> bool { + if self.lines.items.is_empty() { + return false; + } + self.adjusted_max_width = self.max_log_len.saturating_sub(width.into()) + 4; + self.adjust_max_width_text_len = self.adjusted_max_width.to_string().chars().count(); + self.max_log_len + 4 > usize::from(width) + } + /// Add a padding so one char will always be visilbe? - /// +6 is to account for borders & the selection triangle and a little bit of padding pub fn forward(&mut self, width: u16) { let offset = usize::from(self.offset); - self.adjusted_max_width = self.max_log_len.saturating_sub(width.into()) + 6; - if self.adjusted_max_width > 0 && offset < self.adjusted_max_width { - self.offset = self.offset.saturating_add(1); + if self.horizontal_scroll_able(width) { + if self.adjusted_max_width > 0 && offset < self.adjusted_max_width { + self.offset = self.offset.saturating_add(1); + } } } @@ -913,7 +938,7 @@ mod tests { text::{Line, Text}, }; - use crate::{ + use crate::{ app_data::{ContainerImage, Logs, LogsTz, RunningState}, ui::log_sanitizer, }; @@ -1153,4 +1178,38 @@ mod tests { result ); } + + #[test] + /// Test the get_scroll_title methods + fn test_scroll_title() { + let mut logs = Logs::default(); + + let result = logs.get_scroll_title(10); + assert!(result.is_none()); + + let input = "short".to_owned(); + let (tz, _) = LogsTz::splitter(&input); + logs.insert(Text::from(input), tz); + + let result = logs.get_scroll_title(10); + assert!(result.is_none()); + + let input = "2023-01-14T19:13:30.783138328Z Hello world some long line".to_owned(); + let (tz, _) = LogsTz::splitter(&input); + logs.insert(Text::from(input), tz); + + let result = logs.get_scroll_title(10); + assert_eq!(result, Some(" 0/51 → ".to_owned())); + + logs.forward(10); + + let result = logs.get_scroll_title(10); + assert_eq!(result, Some(" ← 1/51 → ".to_owned())); + + for _ in 0..=49 { + logs.forward(10); + } + let result = logs.get_scroll_title(10); + assert_eq!(result, Some(" ← 51/51 ".to_owned())); + } } diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 83c1415..ab7993e 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -638,9 +638,9 @@ impl AppData { } /// If scrolling horiztonally along the logs, display a counter of the position in the in the scroll, `x/y` - pub fn get_scroll_title(&self) -> Option { - self.get_selected_container() - .and_then(|i| i.logs.get_scroll_title()) + pub fn get_scroll_title(&mut self, width: u16) -> Option { + self.get_mut_selected_container() + .and_then(|i| i.logs.get_scroll_title(width)) } /// Increase the logs offset, basically moving an invisible cursor back diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index a42e5d4..52ecc24 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -168,7 +168,7 @@ impl DockerData { (None, None) }; - // TODO is hardcoded eth0 a good idea here? - Could use first() instead? + // TODO is hardcoded eth0 a good idea here? let (rx, tx) = stats.networks.as_ref().map_or((0, 0), |i| { i.get("eth0").map_or((0, 0), |x| { ( diff --git a/src/ui/draw_blocks/logs.rs b/src/ui/draw_blocks/logs.rs index 72a2bcf..61d501f 100644 --- a/src/ui/draw_blocks/logs.rs +++ b/src/ui/draw_blocks/logs.rs @@ -356,7 +356,6 @@ mod tests { insert_logs(&setup); let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - setup .terminal .draw(|f| { diff --git a/src/ui/draw_blocks/mod.rs b/src/ui/draw_blocks/mod.rs index 56f829d..d65a37c 100644 --- a/src/ui/draw_blocks/mod.rs +++ b/src/ui/draw_blocks/mod.rs @@ -158,7 +158,7 @@ pub mod tests { /// Create a FrameData struct from two Arc's, instead of from UI impl From<(&Arc>, &Arc>)> for FrameData { fn from(data: (&Arc>, &Arc>)) -> Self { - let (app_data, gui_data) = (data.0.lock(), data.1.lock()); + let (mut app_data, gui_data) = (data.0.lock(), data.1.lock()); // let container_section_height = app_data.get_container_len(); // let container_section_height = if container_section_height < 12 { @@ -185,7 +185,7 @@ pub mod tests { loading_icon: gui_data.get_loading().to_string(), log_height: gui_data.get_log_height(), log_title: app_data.get_log_title(), - scroll_title: app_data.get_scroll_title(), + scroll_title: app_data.get_scroll_title(gui_data.get_screen_width()), port_max_lens: app_data.get_longest_port(), ports: app_data.get_selected_ports(), selected_panel: gui_data.get_selected_panel(), @@ -216,6 +216,7 @@ pub mod tests { let gui_state = Arc::new(Mutex::new(gui_state)); let fd = FrameData::from((&app_data, &gui_state)); let area = Rect::new(0, 0, w, h); + gui_state.lock().set_screen_width(w); TuiTestSetup { app_data, gui_state, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index b749aed..3ee8d15 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -312,7 +312,7 @@ pub struct FrameData { impl From<&Ui> for FrameData { fn from(ui: &Ui) -> Self { - let (app_data, gui_data) = (ui.app_data.lock(), ui.gui_state.lock()); + let (mut app_data, gui_data) = (ui.app_data.lock(), ui.gui_state.lock()); let (filter_by, filter_term) = app_data.get_filter(); Self { @@ -333,7 +333,7 @@ impl From<&Ui> for FrameData { log_title: app_data.get_log_title(), port_max_lens: app_data.get_longest_port(), ports: app_data.get_selected_ports(), - scroll_title: app_data.get_scroll_title(), + scroll_title: app_data.get_scroll_title(gui_data.get_screen_width()), selected_panel: gui_data.get_selected_panel(), sorted_by: app_data.get_sorted(), status: gui_data.get_status(), From c5bbffdb5f9e800951e4060aa6aee8e00db589aa Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:19:10 +0000 Subject: [PATCH 11/20] feat: ctrl scroll modifier Use the `ctrl` button to scroll by a factor of ten --- .github/screenshot_01.png | Bin 36041 -> 45020 bytes README.md | 3 +- example_config/example.config.jsonc | 7 + example_config/example.config.toml | 13 +- src/app_data/container_state.rs | 3 +- src/config/config.toml | 9 + src/config/keymap_parser.rs | 163 +++++++++++------- src/input_handler/message.rs | 2 +- src/input_handler/mod.rs | 77 ++++++--- src/ui/draw_blocks/help.rs | 57 ++++-- ...blocks__help__tests__draw_blocks_help.snap | 3 +- ...tests__draw_blocks_help_custom_colors.snap | 3 +- ...cks_help_custom_keymap_one_definition.snap | 2 +- ...ks_help_custom_keymap_two_definitions.snap | 2 +- ...w_blocks_help_one_and_two_definitions.snap | 2 +- ...tests__draw_blocks_help_show_timezone.snap | 4 +- ...__draw_blocks_whole_layout_help_panel.snap | 24 +-- src/ui/mod.rs | 5 +- 18 files changed, 249 insertions(+), 130 deletions(-) diff --git a/.github/screenshot_01.png b/.github/screenshot_01.png index 567dd3ad61354aee5114941ed6770920011b3393..f39df4830683b677f0ddeca08532a1f49f667b39 100644 GIT binary patch literal 45020 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 IZTk6t0infoq5uE@ literal 36041 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 CmG&k8 diff --git a/README.md b/README.md index fb9ef37..1c85e30 100644 --- a/README.md +++ b/README.md @@ -105,8 +105,9 @@ In application controls, these, amongst many other settings, can be customized w | 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.| +| ```( ↑ ↓ )``` or ```( j k )``` or ```( PgUp PgDown )``` or ```( Home End )```| Scroll line in selected panel - mouse wheel will also scroll.| | ```( ← → )``` | When logs panel selected, scroll horizontally across the text of the logs.| +| ```( ctrl )``` | Increase scroll speed, used in conjuction scroll keys.| | ```( enter )```| Run selected docker command.| | ```( 1-9 )``` | Sort containers by heading, clicking on headings also sorts the selected column. | | ```( 0 )``` | Stop sorting.| diff --git a/example_config/example.config.jsonc b/example_config/example.config.jsonc index bf0436a..8abb678 100644 --- a/example_config/example.config.jsonc +++ b/example_config/example.config.jsonc @@ -42,6 +42,7 @@ // 3) F1-F12 // 4) backspace, tab, backtab, delete, end, esc, home, insert, pagedown, pageup, left, right, up, down // Each definition can have two keys associated with it + // WARNING "scroll_many" only accepts control, alt, shift, with no secondary option // If any key clashes are found, oxker will revert to it's default keymap "keymap": { // Clear any popup boxes, filter panel, or help panel @@ -74,10 +75,12 @@ "save_logs": [ "s" ], + // TODO "scroll_down_many" will be removed in the next release // Scroll down a list by many "scroll_down_many": [ "pagedown" ], + // TODO rename in next release // Scroll down a list by one item "scroll_down_one": [ "down", @@ -87,14 +90,18 @@ "scroll_end": [ "end" ], + // Modifier to scroll by 10 lines isntead of one, used in conjunction with scroll_up_x/scroll_down_x + "scroll_many": ["control"], // Scroll up to the start of a list "scroll_start": [ "home" ], + // TODO "scroll_up_many" will be removed in the next release // Scroll up a list by many "scroll_up_many": [ "pageup" ], + // TODO rename in next release // Scroll up a list by one item "scroll_up_one": [ "up", diff --git a/example_config/example.config.toml b/example_config/example.config.toml index a7132ac..182e7c5 100644 --- a/example_config/example.config.toml +++ b/example_config/example.config.toml @@ -55,8 +55,10 @@ show_logs = true # 3) F1-F12 # 4) backspace, tab, backtab, delete, end, esc, home, insert, pagedown, pageup, left, right, up, down - # Each definition can have two keys associated with it + +# WARNING "scroll_many" only accepts control, alt, shift, with no secondary option + # If any key clashes are found, oxker will revert to it's default keymap [keymap] @@ -74,16 +76,23 @@ filter_mode = ["/", "F1"] quit = ["q"] # Save logs of selected container to file on disk save_logs = ["s"] +# TODO "scroll_down_many" will be removed in the next release # scroll down a list by many scroll_down_many = ["pagedown"] +# TODO rename in next release # scroll down a list by one item scroll_down_one = ["down", "j"] + # scroll down to the end of a list scroll_end = ["end"] +# Modifier to scroll by 10 lines isntead of one, used in conjunction with scroll_up_x/scroll_down_x +scroll_many = ["control"] # scroll up to the start of a list scroll_start = ["home"] +# TODO "scroll_up_many" will be removed in the next release # scroll up a list by many scroll_up_many = ["pageup"] +# TODO rename in next release # scroll up a list by one item scroll_up_one = ["up", "k"] # Horizontal scroll of the logs @@ -111,7 +120,6 @@ toggle_help = ["h"] toggle_mouse_capture = ["m"] # Reduce the height of the logs list section log_section_height_decrease = ["-"] -# Increase the height of the logs list section log_section_height_increase = ["+"] # Toggle visibility of the log section log_section_toggle = ["\\"] @@ -191,6 +199,7 @@ selected_filter_text = "black" # Highlighted text color highlight = "magenta" + # The color the of Docker commands available for each container [colors.commands] # Background color of panel diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index d5a4ff6..d31550e 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -630,7 +630,6 @@ impl Logs { } } - // TODO test me! /// If scrolling horiztonally along the logs, display a counter of the position in the in the scroll, `x/y` pub fn get_scroll_title(&mut self, width: u16) -> Option { if self.horizontal_scroll_able(width) { @@ -938,7 +937,7 @@ mod tests { text::{Line, Text}, }; - use crate::{ + use crate::{ app_data::{ContainerImage, Logs, LogsTz, RunningState}, ui::log_sanitizer, }; diff --git a/src/config/config.toml b/src/config/config.toml index 25ee594..182e7c5 100644 --- a/src/config/config.toml +++ b/src/config/config.toml @@ -56,6 +56,9 @@ show_logs = true # 4) backspace, tab, backtab, delete, end, esc, home, insert, pagedown, pageup, left, right, up, down # Each definition can have two keys associated with it + +# WARNING "scroll_many" only accepts control, alt, shift, with no secondary option + # If any key clashes are found, oxker will revert to it's default keymap [keymap] @@ -73,17 +76,23 @@ filter_mode = ["/", "F1"] quit = ["q"] # Save logs of selected container to file on disk save_logs = ["s"] +# TODO "scroll_down_many" will be removed in the next release # scroll down a list by many scroll_down_many = ["pagedown"] +# TODO rename in next release # scroll down a list by one item scroll_down_one = ["down", "j"] # scroll down to the end of a list scroll_end = ["end"] +# Modifier to scroll by 10 lines isntead of one, used in conjunction with scroll_up_x/scroll_down_x +scroll_many = ["control"] # scroll up to the start of a list scroll_start = ["home"] +# TODO "scroll_up_many" will be removed in the next release # scroll up a list by many scroll_up_many = ["pageup"] +# TODO rename in next release # scroll up a list by one item scroll_up_one = ["up", "k"] # Horizontal scroll of the logs diff --git a/src/config/keymap_parser.rs b/src/config/keymap_parser.rs index 808cc18..3f62e44 100644 --- a/src/config/keymap_parser.rs +++ b/src/config/keymap_parser.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -use crossterm::event::KeyCode; +use crossterm::event::{KeyCode, KeyModifiers}; /// The macro accepts a list of struct names with key names /// Returns a struct where every key name is an Option, with the correct derived attributes @@ -12,6 +12,7 @@ macro_rules! optional_config_struct { $( $key_name: Option>, )* + pub scroll_many: Option>, } )* }; @@ -24,9 +25,10 @@ macro_rules! config_struct { $( #[derive(Debug, Clone, PartialEq, Eq)] pub struct $struct_name { - $( + $( pub $key_name: (KeyCode, Option), )* + pub scroll_many: KeyModifiers, } )* }; @@ -47,11 +49,15 @@ optional_config_struct!( log_scroll_back, quit, save_logs, + // TODO remove in next release scroll_down_many, + // TODO rename in next release scroll_down_one, scroll_end, scroll_start, + // TODO remove in next release scroll_up_many, + // TODO rename in next release scroll_up_one, select_next_panel, select_previous_panel, @@ -84,11 +90,15 @@ config_struct!( log_scroll_back, quit, save_logs, + // TODO remove in next release scroll_down_many, + // TODO rename in next release scroll_down_one, scroll_end, scroll_start, + // TODO remove in next release scroll_up_many, + // TODO rename in next release scroll_up_one, select_next_panel, select_previous_panel, @@ -122,11 +132,16 @@ impl Keymap { log_scroll_forward: (KeyCode::Right, None), quit: (KeyCode::Char('q'), None), save_logs: (KeyCode::Char('s'), None), + // TODO remove in next release scroll_down_many: (KeyCode::PageDown, None), + // TODO rename in next release scroll_down_one: (KeyCode::Down, Some(KeyCode::Char('j'))), scroll_end: (KeyCode::End, None), scroll_start: (KeyCode::Home, None), + scroll_many: KeyModifiers::CONTROL, + // TODO remove in next release scroll_up_many: (KeyCode::PageUp, None), + // TODO rename in next release scroll_up_one: (KeyCode::Up, Some(KeyCode::Char('k'))), select_next_panel: (KeyCode::Tab, None), select_previous_panel: (KeyCode::BackTab, None), @@ -243,6 +258,10 @@ impl From> for Keymap { &mut keymap.toggle_mouse_capture, &mut clash, ); + // TODO need to check for clashes when using additional modifiers + if let Some(scroll_many) = Self::try_parse_modifier(ck.scroll_many) { + keymap.scroll_many = scroll_many; + } } // A very basic clash check, every key has been inserted into a hashset, and a counter has been increased // if the counter and hashet length don't match, then there's a clash, and we just return the default keymap @@ -255,6 +274,20 @@ impl From> for Keymap { } impl Keymap { + // Allowable key modifiers are only `shift`, `control`, `alt` + fn try_parse_modifier(input: Option>) -> Option { + input.and_then(|input| { + input + .first() + .and_then(|input| match input.to_lowercase().trim() { + "control" => Some(KeyModifiers::CONTROL), + "alt" => Some(KeyModifiers::ALT), + "shift" => Some(KeyModifiers::SHIFT), + _ => None, + }) + }) + } + /// Try to parse a &[String] into a Vec of keycodes, at most the output will have 2 entries /// This might fail on MacOS due to Backspace and Delete working in a different manner as to how they work on Linux & Windows /// I think that on MacOS `Del` becomes `Fwd Del`, and `Backspace` becomes `Delete` @@ -326,7 +359,7 @@ impl Keymap { #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { - use crossterm::event::KeyCode; + use crossterm::event::{KeyCode, KeyModifiers}; use crate::config::keymap_parser::ConfigKeymap; @@ -393,6 +426,7 @@ mod tests { scroll_down_one: None, scroll_end: None, scroll_start: None, + scroll_many: None, scroll_up_many: None, scroll_up_one: None, select_next_panel: None, @@ -423,78 +457,79 @@ mod tests { let input = ConfigKeymap { clear: gen_v(("a", "b")), - delete_confirm: gen_v(("e", "f")), - delete_deny: gen_v(("c", "d")), + delete_confirm: gen_v(("c", "d")), + delete_deny: gen_v(("e", "fd")), exec: gen_v(("g", "h")), filter_mode: gen_v(("i", "j")), - force_redraw: gen_v(("F1", "F2")), - log_section_height_decrease: gen_v(("-", "Z")), - log_section_height_increase: gen_v(("=", "X")), - log_scroll_forward: gen_v(("right", "R")), - log_scroll_back: gen_v(("left", "L")), - log_section_toggle: gen_v(("Y", "W")), - quit: gen_v(("k", "l")), - save_logs: gen_v(("m", "n")), - scroll_down_many: gen_v(("o", "p")), - scroll_down_one: gen_v(("q", "r")), - scroll_end: gen_v(("s", "t")), - scroll_start: gen_v(("u", "v")), - scroll_up_many: gen_v(("w", "x")), - scroll_up_one: gen_v(("y", "z")), - select_next_panel: gen_v(("0", "1")), - select_previous_panel: gen_v(("2", "3")), - sort_by_cpu: gen_v(("F11", "F12")), - sort_by_id: gen_v(("[", "]")), - sort_by_image: gen_v(("A", "B")), - sort_by_memory: gen_v(("/", "\\")), - sort_by_name: gen_v(("4", "5")), - sort_by_rx: gen_v(("C", "D")), - sort_by_state: gen_v(("6", "7")), - sort_by_status: gen_v(("8", "9")), - sort_by_tx: gen_v(("insert", "TAB")), - sort_reset: gen_v(("up", "down")), - toggle_help: gen_v(("home", "end")), - toggle_mouse_capture: gen_v(("pagedown", "PAGEUP")), + force_redraw: gen_v(("k", "l")), + log_section_height_decrease: gen_v(("m", "n")), + log_section_height_increase: gen_v(("o", "p")), + log_scroll_forward: gen_v(("q", "r")), + log_scroll_back: gen_v(("s", "t")), + log_section_toggle: gen_v(("u", "v")), + quit: gen_v(("w", "x")), + save_logs: gen_v(("y", "z")), + scroll_down_many: gen_v(("1", "2")), + scroll_down_one: gen_v(("3", "4")), + scroll_end: gen_v(("5", "6")), + scroll_many: Some(vec!["alt".to_owned()]), + scroll_start: gen_v(("7", "8")), + scroll_up_many: gen_v(("9", "0")), + scroll_up_one: gen_v(("F1", "F2")), + select_next_panel: gen_v(("F3", "F4")), + select_previous_panel: gen_v(("F5", "F6")), + sort_by_cpu: gen_v(("F7", "F8")), + sort_by_id: gen_v(("F9", "F10")), + sort_by_image: gen_v(("F11", "F12")), + sort_by_memory: gen_v(("HOME", "END")), + sort_by_name: gen_v(("UP", "DOWN")), + sort_by_rx: gen_v(("LEFT", "RIGHT")), + sort_by_state: gen_v(("[", "]")), + sort_by_status: gen_v(("INSERTt", "TAB")), + sort_by_tx: gen_v(("PAGEDOWN", "PAGEUP")), + sort_reset: gen_v((",", ".")), + toggle_help: gen_v(("-", "=")), + toggle_mouse_capture: gen_v(("\\", "/")), }; let result = Keymap::from(Some(input)); let expected = Keymap { clear: (KeyCode::Char('a'), Some(KeyCode::Char('b'))), - delete_deny: (KeyCode::Char('c'), Some(KeyCode::Char('d'))), - delete_confirm: (KeyCode::Char('e'), Some(KeyCode::Char('f'))), - force_redraw: (KeyCode::F(1), Some(KeyCode::F(2))), - log_section_height_decrease: (KeyCode::Char('-'), Some(KeyCode::Char('Z'))), - log_section_height_increase: (KeyCode::Char('='), Some(KeyCode::Char('X'))), - log_section_toggle: (KeyCode::Char('Y'), Some(KeyCode::Char('W'))), - log_scroll_forward: (KeyCode::Right, Some(KeyCode::Char('R'))), - log_scroll_back: (KeyCode::Left, Some(KeyCode::Char('L'))), + delete_deny: (KeyCode::Char('e'), None), + delete_confirm: (KeyCode::Char('c'), Some(KeyCode::Char('d'))), exec: (KeyCode::Char('g'), Some(KeyCode::Char('h'))), filter_mode: (KeyCode::Char('i'), Some(KeyCode::Char('j'))), - quit: (KeyCode::Char('k'), Some(KeyCode::Char('l'))), - save_logs: (KeyCode::Char('m'), Some(KeyCode::Char('n'))), - scroll_down_many: (KeyCode::Char('o'), Some(KeyCode::Char('p'))), - scroll_down_one: (KeyCode::Char('q'), Some(KeyCode::Char('r'))), - scroll_end: (KeyCode::Char('s'), Some(KeyCode::Char('t'))), - scroll_start: (KeyCode::Char('u'), Some(KeyCode::Char('v'))), - scroll_up_many: (KeyCode::Char('w'), Some(KeyCode::Char('x'))), - scroll_up_one: (KeyCode::Char('y'), Some(KeyCode::Char('z'))), - select_next_panel: (KeyCode::Char('0'), Some(KeyCode::Char('1'))), - select_previous_panel: (KeyCode::Char('2'), Some(KeyCode::Char('3'))), - sort_by_name: (KeyCode::Char('4'), Some(KeyCode::Char('5'))), - sort_by_state: (KeyCode::Char('6'), Some(KeyCode::Char('7'))), - sort_by_status: (KeyCode::Char('8'), Some(KeyCode::Char('9'))), - sort_by_cpu: (KeyCode::F(11), Some(KeyCode::F(12))), - sort_by_memory: (KeyCode::Char('/'), Some(KeyCode::Char('\\'))), - sort_by_id: (KeyCode::Char('['), Some(KeyCode::Char(']'))), - sort_by_image: (KeyCode::Char('A'), Some(KeyCode::Char('B'))), - sort_by_rx: (KeyCode::Char('C'), Some(KeyCode::Char('D'))), - sort_by_tx: (KeyCode::Insert, Some(KeyCode::Tab)), - sort_reset: (KeyCode::Up, Some(KeyCode::Down)), - toggle_help: (KeyCode::Home, Some(KeyCode::End)), - toggle_mouse_capture: (KeyCode::PageDown, Some(KeyCode::PageUp)), + force_redraw: (KeyCode::Char('k'), Some(KeyCode::Char('l'))), + log_section_height_increase: (KeyCode::Char('o'), Some(KeyCode::Char('p'))), + log_section_height_decrease: (KeyCode::Char('m'), Some(KeyCode::Char('n'))), + log_section_toggle: (KeyCode::Char('u'), Some(KeyCode::Char('v'))), + log_scroll_forward: (KeyCode::Char('q'), Some(KeyCode::Char('r'))), + log_scroll_back: (KeyCode::Char('s'), Some(KeyCode::Char('t'))), + quit: (KeyCode::Char('w'), Some(KeyCode::Char('x'))), + save_logs: (KeyCode::Char('y'), Some(KeyCode::Char('z'))), + scroll_down_many: (KeyCode::Char('1'), Some(KeyCode::Char('2'))), + scroll_down_one: (KeyCode::Char('3'), Some(KeyCode::Char('4'))), + scroll_end: (KeyCode::Char('5'), Some(KeyCode::Char('6'))), + scroll_start: (KeyCode::Char('7'), Some(KeyCode::Char('8'))), + scroll_up_many: (KeyCode::Char('9'), Some(KeyCode::Char('0'))), + scroll_up_one: (KeyCode::F(1), Some(KeyCode::F(2))), + select_next_panel: (KeyCode::F(3), Some(KeyCode::F(4))), + select_previous_panel: (KeyCode::F(5), Some(KeyCode::F(6))), + sort_by_name: (KeyCode::Up, Some(KeyCode::Down)), + sort_by_state: (KeyCode::Char('['), Some(KeyCode::Char(']'))), + sort_by_status: (KeyCode::Tab, None), + sort_by_cpu: (KeyCode::F(7), Some(KeyCode::F(8))), + sort_by_memory: (KeyCode::Home, Some(KeyCode::End)), + sort_by_id: (KeyCode::F(9), Some(KeyCode::F(10))), + sort_by_image: (KeyCode::F(11), Some(KeyCode::F(12))), + sort_by_rx: (KeyCode::Left, Some(KeyCode::Right)), + sort_by_tx: (KeyCode::PageDown, Some(KeyCode::PageUp)), + sort_reset: (KeyCode::Char(','), Some(KeyCode::Char('.'))), + toggle_help: (KeyCode::Char('-'), Some(KeyCode::Char('='))), + toggle_mouse_capture: (KeyCode::Char('\\'), Some(KeyCode::Char('/'))), + scroll_many: KeyModifiers::ALT, }; - assert_eq!(expected, result); } } diff --git a/src/input_handler/message.rs b/src/input_handler/message.rs index ba50101..1d5ab4a 100644 --- a/src/input_handler/message.rs +++ b/src/input_handler/message.rs @@ -3,5 +3,5 @@ use crossterm::event::{KeyCode, KeyModifiers, MouseEvent}; #[derive(Debug, Clone, Copy)] pub enum InputMessages { ButtonPress((KeyCode, KeyModifiers)), - MouseEvent(MouseEvent), + MouseEvent((MouseEvent, KeyModifiers)), } diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index faa2d03..fcf098c 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -67,7 +67,7 @@ impl InputHandler { while let Some(message) = self.rx.recv().await { match message { InputMessages::ButtonPress(key) => self.button_press(key.0, key.1).await, - InputMessages::MouseEvent(mouse_event) => { + InputMessages::MouseEvent((mouse_event, modifider)) => { let status = self.gui_state.lock().get_status(); let contains = |s: Status| status.contains(&s); @@ -78,7 +78,7 @@ impl InputHandler { | !contains(Status::DeleteConfirm) | !contains(Status::Filter) { - self.mouse_press(mouse_event); + self.mouse_press(mouse_event, modifider); } } } @@ -285,20 +285,33 @@ impl InputHandler { } } + /// If keymap.scroll_modifier is pressed, return 10, else return 1, to speed up scrolling + fn get_modifier_total(&self, modifier: KeyModifiers) -> u8 { + if modifier == self.keymap.scroll_many { + 10 + } else { + 1 + } + } + /// Advance the "cursor" along the logs - fn logs_forward(&self) { + fn logs_forward(&self, modifier: KeyModifiers) { let panel = self.gui_state.lock().get_selected_panel(); if panel == SelectablePanel::Logs { - let width = self.gui_state.lock().get_screen_width(); - self.app_data.lock().log_forward(width); + for _ in 0..self.get_modifier_total(modifier) { + let width = self.gui_state.lock().get_screen_width(); + self.app_data.lock().log_forward(width); + } } } /// Retreat the "cursor" along the logs - fn logs_back(&self) { + fn logs_back(&self, modifier: KeyModifiers) { let panel = self.gui_state.lock().get_selected_panel(); if panel == SelectablePanel::Logs { - self.app_data.lock().log_back(); + for _ in 0..self.get_modifier_total(modifier) { + self.app_data.lock().log_back(); + } } } @@ -489,7 +502,7 @@ impl InputHandler { /// Handle button presses in all other scenarios #[allow(clippy::cognitive_complexity)] - async fn handle_others(&mut self, key_code: KeyCode) { + async fn handle_others(&mut self, key_code: KeyCode, modifier: KeyModifiers) { self.handle_sort(key_code); // shift key plus arrows match key_code { @@ -559,28 +572,28 @@ impl InputHandler { _ if self.keymap.scroll_up_one.0 == key_code || self.keymap.scroll_up_one.1 == Some(key_code) => { - self.scroll_up(); + self.scroll_up(modifier); } _ if self.keymap.scroll_up_many.0 == key_code || self.keymap.scroll_up_many.1 == Some(key_code) => { for _ in 0..=6 { - self.scroll_up(); + self.scroll_up(modifier); } } _ if self.keymap.scroll_down_one.0 == key_code || self.keymap.scroll_down_one.1 == Some(key_code) => { - self.scroll_down(); + self.scroll_down(modifier); } _ if self.keymap.scroll_down_many.0 == key_code || self.keymap.scroll_down_many.1 == Some(key_code) => { for _ in 0..=6 { - self.scroll_down(); + self.scroll_down(modifier); } } @@ -594,13 +607,13 @@ impl InputHandler { _ if self.keymap.log_scroll_back.0 == key_code || self.keymap.log_scroll_back.1 == Some(key_code) => { - self.logs_back(); + self.logs_back(modifier); } _ if self.keymap.log_scroll_forward.0 == key_code || self.keymap.log_scroll_forward.1 == Some(key_code) => { - self.logs_forward(); + self.logs_forward(modifier); } KeyCode::Enter => self.enter_key().await, @@ -637,7 +650,7 @@ impl InputHandler { } else if contains_delete { self.handle_delete(key_code).await; } else { - self.handle_others(key_code).await; + self.handle_others(key_code, key_modifier).await; } } } @@ -662,7 +675,7 @@ impl InputHandler { } /// Handle mouse button events - fn mouse_press(&self, mouse_event: MouseEvent) { + fn mouse_press(&self, mouse_event: MouseEvent, modifier: KeyModifiers) { let status = self.gui_state.lock().get_status(); if status.contains(&Status::Help) { let mouse_point = Rect::new(mouse_event.column, mouse_event.row, 1, 1); @@ -672,8 +685,8 @@ impl InputHandler { } } else { match mouse_event.kind { - MouseEventKind::ScrollUp => self.scroll_up(), - MouseEventKind::ScrollDown => self.scroll_down(), + MouseEventKind::ScrollUp => self.scroll_up(modifier), + MouseEventKind::ScrollDown => self.scroll_down(modifier), MouseEventKind::Down(MouseButton::Left) => { let mouse_point = Rect::new(mouse_event.column, mouse_event.row, 1, 1); let header = self.gui_state.lock().get_intersect_header(mouse_point); @@ -693,21 +706,37 @@ impl InputHandler { } /// Change state to next, depending which panel is currently in focus - fn scroll_down(&self) { + fn scroll_down(&self, modifier: KeyModifiers) { let selected_panel = self.gui_state.lock().get_selected_panel(); match selected_panel { - SelectablePanel::Containers => self.app_data.lock().containers_next(), - SelectablePanel::Logs => self.app_data.lock().log_next(), + SelectablePanel::Containers => { + for _ in 0..self.get_modifier_total(modifier) { + self.app_data.lock().containers_next(); + } + } + SelectablePanel::Logs => { + for _ in 0..self.get_modifier_total(modifier) { + self.app_data.lock().log_next(); + } + } SelectablePanel::Commands => self.app_data.lock().docker_controls_next(), } } /// Change state to previous, depending which panel is currently in focus - fn scroll_up(&self) { + fn scroll_up(&self, modifier: KeyModifiers) { let selected_panel = self.gui_state.lock().get_selected_panel(); match selected_panel { - SelectablePanel::Containers => self.app_data.lock().containers_previous(), - SelectablePanel::Logs => self.app_data.lock().log_previous(), + SelectablePanel::Containers => { + for _ in 0..self.get_modifier_total(modifier) { + self.app_data.lock().containers_previous(); + } + } + SelectablePanel::Logs => { + for _ in 0..self.get_modifier_total(modifier) { + self.app_data.lock().log_previous(); + } + } SelectablePanel::Commands => self.app_data.lock().docker_controls_previous(), } } diff --git a/src/ui/draw_blocks/help.rs b/src/ui/draw_blocks/help.rs index 0694ddd..83a3051 100644 --- a/src/ui/draw_blocks/help.rs +++ b/src/ui/draw_blocks/help.rs @@ -109,13 +109,18 @@ impl HelpInfo { button_item("PgUp PgDown"), or(), button_item("Home End"), - button_desc("change selected line"), + button_desc("scroll vertically"), ]), Line::from(vec![ space(), button_item("← →"), button_desc("horizontal scroll across logs"), ]), + Line::from(vec![ + space(), + button_item("ctrl"), + button_desc("increase scroll speed, used in conjuction scroll keys"), + ]), Line::from(vec![ space(), button_item("enter"), @@ -280,6 +285,11 @@ impl HelpInfo { or_secondary(km.scroll_start, "scroll list to start"), or_secondary(km.log_scroll_forward, "horizontal scroll logs right"), or_secondary(km.log_scroll_back, "horizontal scroll logs left"), + Line::from(vec![ + space(), + button_item(km.scroll_many.to_string().as_str()), + button_desc("increase scroll speed, used in conjuction scroll keys"), + ]), Line::from(vec![ space(), button_item("enter"), @@ -440,7 +450,7 @@ pub fn draw( #[allow(clippy::unwrap_used, clippy::too_many_lines)] mod tests { use crate::config::{AppColors, Keymap}; - use crossterm::event::KeyCode; + use crossterm::event::{KeyCode, KeyModifiers}; use insta::assert_snapshot; use jiff::tz::TimeZone; use ratatui::style::{Color, Modifier}; @@ -449,9 +459,10 @@ mod tests { #[test] /// This will cause issues once the version has more than the current 5 chars (0.5.0) + /// This test is incredibly annoying /// println!("{} {} {} {} {}", row_index, result_cell_index, result_cell.symbol(), result_cell.bg, result_cell.fg); fn test_draw_blocks_help() { - let mut setup = test_setup(87, 36, true, true); + let mut setup = test_setup(87, 37, true, true); let tz = setup.app_data.lock().config.timezone.clone(); setup @@ -471,9 +482,17 @@ mod tests { for (row_index, result_row) in get_result(&setup) { for (result_cell_index, result_cell) in result_row.iter().enumerate() { + println!( + "{} {} {} {} {}", + row_index, + result_cell_index, + result_cell.symbol(), + result_cell.bg, + result_cell.fg + ); match (row_index, result_cell_index) { // first & last row, and first & last char on each row, is reset/reset, making sure that the help info is centered in the given area - (0 | 35, _) | (0..=34, 0 | 86) => { + (0 | 36, _) | (0..=35, 0 | 86) => { assert_eq!(result_cell.bg, Color::Reset); assert_eq!(result_cell.fg, Color::Reset); } @@ -487,15 +506,16 @@ mod tests { | (12, 19..=66) | (14, 2..=10 | 13..=27) | (15, 2..=10 | 13..=21 | 24..=40 | 43..=56) - | (16 | 26 | 28, 2..=10) - | (17 | 25, 2..=12) - | (18 | 19 | 20 | 21 | 22 | 24 | 27 | 29, 2..=8) - | (23, 2..=9 | 12..=18) => { + | (16 | 27 | 29, 2..=10) + | (17, 2..=11) + | (18 | 26, 2..=12) + | (19 | 20 | 21 | 22 | 24 | 25 | 28 | 23 | 30, 2..=8) + | (24, 2..=9 | 12..=18) => { assert_eq!(result_cell.bg, Color::Magenta); assert_eq!(result_cell.fg, Color::White); } // The URL is yellow and underlined - (32, 25..=60) => { + (33, 25..=60) => { assert_eq!(result_cell.bg, Color::Magenta); assert_eq!(result_cell.fg, Color::White); assert_eq!(result_cell.modifier, Modifier::UNDERLINED); @@ -512,9 +532,10 @@ mod tests { #[test] /// Test that the help panel gets drawn with custom colors + /// This test is incredibly annoying /// println!("{} {} {} {} {}", row_index, result_cell_index, result_cell.symbol(), result_cell.bg, result_cell.fg); fn test_draw_blocks_help_custom_colors() { - let mut setup = test_setup(87, 36, true, true); + let mut setup = test_setup(87, 37, true, true); let mut colors = AppColors::new(); let tz = setup.app_data.lock().config.timezone.clone(); @@ -540,7 +561,7 @@ mod tests { for (result_cell_index, result_cell) in result_row.iter().enumerate() { match (row_index, result_cell_index) { // first & last row, and first & last char on each row, is reset/reset, making sure that the help info is centered in the given area - (0 | 35, _) | (0..=34, 0 | 86) => { + (0 | 36, _) | (0..=35, 0 | 86) => { assert_eq!(result_cell.bg, Color::Reset); assert_eq!(result_cell.fg, Color::Reset); } @@ -554,15 +575,16 @@ mod tests { | (12, 19..=66) | (14, 2..=10 | 13..=27) | (15, 2..=10 | 13..=21 | 24..=40 | 43..=56) - | (16 | 26 | 28, 2..=10) - | (17 | 25, 2..=12) - | (18 | 19 | 20 | 21 | 22 | 24 | 27 | 29, 2..=8) - | (23, 2..=9 | 12..=18) => { + | (16 | 27 | 29, 2..=10) + | (17, 2..=11) + | (18 | 26, 2..=12) + | (19 | 20 | 21 | 22 | 24 | 25 | 28 | 23 | 30, 2..=8) + | (24, 2..=9 | 12..=18) => { assert_eq!(result_cell.bg, Color::Black); assert_eq!(result_cell.fg, Color::Yellow); } // The URL is yellow and underlined - (32, 25..=60) => { + (33, 25..=60) => { assert_eq!(result_cell.bg, Color::Black); assert_eq!(result_cell.fg, Color::Yellow); assert_eq!(result_cell.modifier, Modifier::UNDERLINED); @@ -599,6 +621,7 @@ mod tests { scroll_down_many: (KeyCode::Char('n'), None), scroll_down_one: (KeyCode::Char('o'), None), scroll_end: (KeyCode::Char('p'), None), + scroll_many: KeyModifiers::ALT, scroll_start: (KeyCode::Char('q'), None), scroll_up_many: (KeyCode::Char('r'), None), scroll_up_one: (KeyCode::Char('s'), None), @@ -650,6 +673,7 @@ mod tests { scroll_down_many: (KeyCode::Char('m'), Some(KeyCode::Char('M'))), scroll_down_one: (KeyCode::Char('n'), Some(KeyCode::Char('N'))), scroll_end: (KeyCode::Char('o'), Some(KeyCode::Char('O'))), + scroll_many: KeyModifiers::ALT, scroll_start: (KeyCode::Char('p'), Some(KeyCode::Char('P'))), scroll_up_many: (KeyCode::Char('q'), Some(KeyCode::Char('Q'))), scroll_up_one: (KeyCode::Char('r'), Some(KeyCode::Char('R'))), @@ -701,6 +725,7 @@ mod tests { scroll_down_many: (KeyCode::Char('n'), None), scroll_down_one: (KeyCode::Char('o'), Some(KeyCode::Char('O'))), scroll_end: (KeyCode::Char('p'), None), + scroll_many: KeyModifiers::ALT, scroll_start: (KeyCode::Char('q'), Some(KeyCode::Char('Q'))), scroll_up_many: (KeyCode::Char('r'), None), scroll_up_one: (KeyCode::Char('s'), Some(KeyCode::Char('S'))), diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help.snap index de059d4..ad841ca 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help.snap @@ -17,8 +17,9 @@ expression: setup.terminal.backend() " │ A simple tui to view & control docker containers │ " " │ │ " " │ ( tab ) or ( shift+tab ) change panels │ " -" │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) change selected line │ " +" │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) scroll vertically │ " " │ ( ← → ) horizontal scroll across logs │ " +" │ ( ctrl ) increase scroll speed, used in conjuction scroll keys │ " " │ ( enter ) send docker container command │ " " │ ( e ) exec into a container │ " " │ ( f ) force clear the screen & redraw the gui │ " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_colors.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_colors.snap index de059d4..ad841ca 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_colors.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_colors.snap @@ -17,8 +17,9 @@ expression: setup.terminal.backend() " │ A simple tui to view & control docker containers │ " " │ │ " " │ ( tab ) or ( shift+tab ) change panels │ " -" │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) change selected line │ " +" │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) scroll vertically │ " " │ ( ← → ) horizontal scroll across logs │ " +" │ ( ctrl ) increase scroll speed, used in conjuction scroll keys │ " " │ ( enter ) send docker container command │ " " │ ( e ) exec into a container │ " " │ ( f ) force clear the screen & redraw the gui │ " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_one_definition.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_one_definition.snap index eee704c..f8933a4 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_one_definition.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_one_definition.snap @@ -25,6 +25,7 @@ expression: setup.terminal.backend() " │ ( q ) scroll list to start │ " " │ ( h ) horizontal scroll logs right │ " " │ ( g ) horizontal scroll logs left │ " +" │ ( Alt ) increase scroll speed, used in conjuction scroll keys │ " " │ ( enter ) send docker container command │ " " │ ( d ) exec into a container │ " " │ ( f ) force clear the screen & redraw the gui │ " @@ -50,5 +51,4 @@ expression: setup.terminal.backend() " │ │ " " │ currently an early work in progress, all and any input appreciated │ " " │ https://github.com/mrjackwills/oxker │ " -" │ │ " " ╰────────────────────────────────────────────────────────────────────────────────────╯ " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_two_definitions.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_two_definitions.snap index 418e684..40866fb 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_two_definitions.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_two_definitions.snap @@ -25,6 +25,7 @@ expression: setup.terminal.backend() " │ ( p ) or ( P ) scroll list to start │ " " │ ( g ) or ( G ) horizontal scroll logs right │ " " │ ( f ) or ( F ) horizontal scroll logs left │ " +" │ ( Alt ) increase scroll speed, used in conjuction scroll keys │ " " │ ( enter ) send docker container command │ " " │ ( d ) or ( D ) exec into a container │ " " │ ( f ) or ( F ) force clear the screen & redraw the gui │ " @@ -50,5 +51,4 @@ expression: setup.terminal.backend() " │ │ " " │ currently an early work in progress, all and any input appreciated │ " " │ https://github.com/mrjackwills/oxker │ " -" │ │ " " ╰────────────────────────────────────────────────────────────────────────────────────────────────────╯ " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_one_and_two_definitions.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_one_and_two_definitions.snap index 15f2de0..3694cac 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_one_and_two_definitions.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_one_and_two_definitions.snap @@ -25,6 +25,7 @@ expression: setup.terminal.backend() " │ ( q ) or ( Q ) scroll list to start │ " " │ ( h ) horizontal scroll logs right │ " " │ ( g ) or ( G ) horizontal scroll logs left │ " +" │ ( Alt ) increase scroll speed, used in conjuction scroll keys │ " " │ ( enter ) send docker container command │ " " │ ( d ) exec into a container │ " " │ ( f ) force clear the screen & redraw the gui │ " @@ -50,5 +51,4 @@ expression: setup.terminal.backend() " │ │ " " │ currently an early work in progress, all and any input appreciated │ " " │ https://github.com/mrjackwills/oxker │ " -" │ │ " " ╰────────────────────────────────────────────────────────────────────────────────────────────╯ " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_show_timezone.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_show_timezone.snap index 82193cb..67b6242 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_show_timezone.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_show_timezone.snap @@ -18,8 +18,9 @@ expression: setup.terminal.backend() " │ logs timezone: Asia/Tokyo │ " " │ │ " " │ ( tab ) or ( shift+tab ) change panels │ " -" │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) change selected line │ " +" │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) scroll vertically │ " " │ ( ← → ) horizontal scroll across logs │ " +" │ ( ctrl ) increase scroll speed, used in conjuction scroll keys │ " " │ ( enter ) send docker container command │ " " │ ( e ) exec into a container │ " " │ ( f ) force clear the screen & redraw the gui │ " @@ -37,5 +38,4 @@ expression: setup.terminal.backend() " │ currently an early work in progress, all and any input appreciated │ " " │ https://github.com/mrjackwills/oxker │ " " │ │ " -" │ │ " " ╰───────────────────────────────────────────────────────────────────────────────────╯ " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_help_panel.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_help_panel.snap index 65aed11..f6603f1 100644 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_help_panel.snap +++ b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_help_panel.snap @@ -4,23 +4,23 @@ expression: setup.terminal.backend() --- " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) exit help " "╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮" -"│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │" Hidden by multi-width symbols: [(2, " ")] -"│ container_2 ✓ running Up 2 ho╭ 0.00.000 ──────────────────────────────────────────────────────────────────────────╮ ││ restart │" -"│ container_3 ✓ running Up 3 ho│ │ ││ stop │" +"│⚪ container_1 ✓ running Up 1 ho╭ 0.00.000 ──────────────────────────────────────────────────────────────────────────╮ ││▶ pause │" Hidden by multi-width symbols: [(2, " ")] +"│ container_2 ✓ running Up 2 ho│ │ ││ restart │" +"│ container_3 ✓ running Up 3 ho│ 88 │ ││ stop │" "│ │ 88 │ ││ delete │" "│ │ 88 │ ││ │" -"╰────────────────────────────────────│ 88 │────────────────────╯╰──────────────╯" -"╭ Logs 3/3 - container_1 - image_1 ──│ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │────────────────────────────────────╮" -"│ line 1 │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ │" -"│ line 2 │ 8b d8 )888( 8888[ 8PP""""""" 88 │ │" -"│▶ line 3 │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ │" -"│ │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ │" +"╰────────────────────────────────────│ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │────────────────────╯╰──────────────╯" +"╭ Logs 3/3 - container_1 - image_1 ──│ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │────────────────────────────────────╮" +"│ line 1 │ 8b d8 )888( 8888[ 8PP""""""" 88 │ │" +"│ line 2 │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ │" +"│▶ line 3 │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ │" "│ │ │ │" "│ │ A simple tui to view & control docker containers │ │" "│ │ │ │" "│ │ ( tab ) or ( shift+tab ) change panels │ │" -"│ │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) change selected line │ │" +"│ │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) scroll vertically │ │" "│ │ ( ← → ) horizontal scroll across logs │ │" +"│ │ ( ctrl ) increase scroll speed, used in conjuction scroll keys │ │" "│ │ ( enter ) send docker container command │ │" "│ │ ( e ) exec into a container │ │" "│ │ ( f ) force clear the screen & redraw the gui │ │" @@ -38,7 +38,7 @@ expression: setup.terminal.backend() "│ │ •• • │ currently an early work in progress, all and any input appreciated │ ││127.0.0.1 8003 8003│" "│ │ • • │ https://github.com/mrjackwills/oxker │ ││ │" "│ │ •• • • │ │ ││ │" -"│ │• •• ╰────────────────────────────────────────────────────────────────────────────────────╯ ││ │" -"│ │• • ││ │• • ││ │" +"│ │• •• │ │ ││ │" +"│ │• • ╰────────────────────────────────────────────────────────────────────────────────────╯ ││ │" "│ │ ││ │ ││ │" "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯" diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 3ee8d15..c46dacf 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -255,7 +255,10 @@ impl Ui { event::MouseEventKind::Down(_) | event::MouseEventKind::ScrollDown | event::MouseEventKind::ScrollUp => { - self.input_tx.send(InputMessages::MouseEvent(m)).await.ok(); + self.input_tx + .send(InputMessages::MouseEvent((m, m.modifiers))) + .await + .ok(); } _ => (), } From bb1fe019be48ff120e357b5cf999abe85b403dc9 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:44:13 +0000 Subject: [PATCH 12/20] docs: changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2863f28..64b17a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,9 @@ + Rust 1.89 linting, [79d19ceeb81ae60bc5562683e405d6e74e6f2578] ### Features -+ horizontally scroll across logs. By default use `←` & `→` keys to traverse horizontally across the lines when logs panel selected. Updated `config.toml` with `log_scroll_forward` and `log_scroll_back` [c190f0206cc55b8e45b8373f9be954e828c18b3b] ++ horizontally scroll across logs. By default use `←` & `→` keys to traverse horizontally across the lines when logs panel selected. Updated `config.toml` with `log_scroll_forward` and `log_scroll_back` [c190f0206cc55b8e45b8373f9be954e828c18b3b], [8939ac0345326633e794cc10a981a1f3c5c07549] + Force clear screen & redraw of UI. By default uses `f` key, `config.toml` updated with `force_redraw` [50edbc0cc09db864835fe81a03cba8eadafe548b] ++ `ctrl` key act as a modifier to scroll by a factor of ten, `config.toml` updated with `scroll_modifier`, will remove `scroll_down_many` & `scroll_down_up` in next release, [c5bbffdb5f9e800951e4060aa6aee8e00db589aa] ### Refactors + remove macos cfg none-const functions, Zigbuild now uses Rust 1.87.0, [eb686e2c952e04da74b3e12c0bfa015ec4615e1d] From 2c0833a21c52ab7cf0cac7cdbebcbda4c5563688 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:44:32 +0000 Subject: [PATCH 13/20] chore: docker-compose updated --- docker/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 7af255b..7c58aac 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -4,7 +4,7 @@ networks: name: oxker-examaple-net services: postgres: - image: postgres:alpine3.21 + image: postgres:17-alpine container_name: postgres environment: - POSTGRES_PASSWORD=never_use_this_password_in_production @@ -18,7 +18,7 @@ services: limits: memory: 1024M redis: - image: redis:alpine3.21 + image: redis:latest container_name: redis ipc: private restart: always From 7669250c1ea89973b160a9ab72fce3b6e729fd99 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:51:36 +0000 Subject: [PATCH 14/20] docs: changelog --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64b17a5..64b1ff3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ ### Chores -+ dependencies updated, [ced885e0128b6d5d3a3c7cb97d7e53bc2da64893] ++ Dependencies updated, [ced885e0128b6d5d3a3c7cb97d7e53bc2da64893] + Rust 1.89 linting, [79d19ceeb81ae60bc5562683e405d6e74e6f2578] ### Features -+ horizontally scroll across logs. By default use `←` & `→` keys to traverse horizontally across the lines when logs panel selected. Updated `config.toml` with `log_scroll_forward` and `log_scroll_back` [c190f0206cc55b8e45b8373f9be954e828c18b3b], [8939ac0345326633e794cc10a981a1f3c5c07549] ++ Horizontally scroll across logs. By default use `←` & `→` keys to traverse horizontally across the lines when logs panel selected. Updated `config.toml` with `log_scroll_forward` and `log_scroll_back` [c190f0206cc55b8e45b8373f9be954e828c18b3b], [8939ac0345326633e794cc10a981a1f3c5c07549] + Force clear screen & redraw of UI. By default uses `f` key, `config.toml` updated with `force_redraw` [50edbc0cc09db864835fe81a03cba8eadafe548b] -+ `ctrl` key act as a modifier to scroll by a factor of ten, `config.toml` updated with `scroll_modifier`, will remove `scroll_down_many` & `scroll_down_up` in next release, [c5bbffdb5f9e800951e4060aa6aee8e00db589aa] ++ Increase scroll speed using the `ctrl` key in conjuction with a scroll key, `config.toml` updated with `scroll_modifier`. The next release will remove `scroll_down_many` & `scroll_down_up` keys, [c5bbffdb5f9e800951e4060aa6aee8e00db589aa] ### Refactors + remove macos cfg none-const functions, Zigbuild now uses Rust 1.87.0, [eb686e2c952e04da74b3e12c0bfa015ec4615e1d] From f9b40ea03d0e70e235c28646ff3f9ebb468a904d Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:52:47 +0000 Subject: [PATCH 15/20] chore: dependencies updated --- Cargo.lock | 62 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a00396..f094380 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,6 +94,12 @@ version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -123,9 +129,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" [[package]] name = "bollard" @@ -207,18 +213,18 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.32" +version = "1.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" +checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" dependencies = [ "shlex", ] [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "chrono" @@ -520,9 +526,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -685,19 +691,21 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -872,9 +880,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1240,9 +1248,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" @@ -1297,9 +1305,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.97" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -1505,9 +1513,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -1692,9 +1700,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.105" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -1714,18 +1722,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.14" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.14" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", @@ -1969,9 +1977,9 @@ checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "url" -version = "2.5.4" +version = "2.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "137a3c834eaf7139b73688502f3f1141a0337c5d8e4d9b536f9b8c796e26a7c4" dependencies = [ "form_urlencoded", "idna", From 9fb2de74cec56de1e055101d82d753a9e81fc6e4 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:53:40 +0000 Subject: [PATCH 16/20] docs: changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64b1ff3..1ea4dda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ### Chores -+ Dependencies updated, [ced885e0128b6d5d3a3c7cb97d7e53bc2da64893] ++ Dependencies updated, [ced885e0128b6d5d3a3c7cb97d7e53bc2da64893], [f9b40ea03d0e70e235c28646ff3f9ebb468a904d] + Rust 1.89 linting, [79d19ceeb81ae60bc5562683e405d6e74e6f2578] ### Features From 6573af1ed7d382a81c1305397e904066bb8395a8 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 21 Aug 2025 19:20:33 +0000 Subject: [PATCH 17/20] chore: GitHub action updated --- .github/workflows/create_release_and_build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/create_release_and_build.yml b/.github/workflows/create_release_and_build.yml index c3e89ca..82f36bc 100644 --- a/.github/workflows/create_release_and_build.yml +++ b/.github/workflows/create_release_and_build.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 # Install stable rust, and associated tools - name: install rust @@ -82,10 +82,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup | Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 - name: Update Release uses: ncipollo/release-action@v1 @@ -107,7 +107,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Login to GitHub Container Registry uses: docker/login-action@v3 @@ -152,7 +152,7 @@ jobs: steps: - name: update rust stable run: rustup update stable - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Publish Dry Run run: cargo publish --dry-run @@ -165,7 +165,7 @@ jobs: steps: - name: update rust stable run: rustup update stable - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Publish run: cargo publish From 08384200558fa1b9d378ea62ea832708caebaa91 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 21 Aug 2025 21:19:20 +0000 Subject: [PATCH 18/20] chore: workflow updated --- .../workflows/create_release_and_build.yml | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/.github/workflows/create_release_and_build.yml b/.github/workflows/create_release_and_build.yml index 82f36bc..7b03691 100644 --- a/.github/workflows/create_release_and_build.yml +++ b/.github/workflows/create_release_and_build.yml @@ -121,26 +121,25 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - - name: Write release version to env - run: | - CURRENT_SEMVER=${GITHUB_REF_NAME#v} - echo "CURRENT_SEMVER=$CURRENT_SEMVER" >> $GITHUB_ENV - - - uses: docker/setup-buildx-action@v3 - id: buildx + - name: Build and push Docker image + uses: docker/build-push-action@v6 with: - install: true - - name: Build for Dockerhub & ghcr.io - run: | - docker build --platform linux/arm/v6,linux/arm64,linux/amd64 \ - -t ${{ secrets.DOCKERHUB_USERNAME }}/oxker:latest \ - -t ${{ secrets.DOCKERHUB_USERNAME }}/oxker:${{env.CURRENT_SEMVER}} \ - -t ghcr.io/${{ github.repository_owner }}/${{ github.ref_name }}:latest \ - -t ghcr.io/${{ github.repository_owner }}/${{ github.ref_name }}:${{env.CURRENT_SEMVER}} \ - --provenance=false --sbom=false \ - --push \ - -f containerised/Dockerfile . + context: . + file: ./containerised/Dockerfile + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/{{ github.event.repository.name }}:latest + ${{ secrets.DOCKERHUB_USERNAME }}/{{ github.event.repository.name }}:${{env.CURRENT_SEMVER}} + ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest + ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{ env.CURRENT_SEMVER }} + platforms: linux/arm/v6,linux/arm64,linux/amd64 + provenance: false + sbom: false + ######################## # Publish to crates.io # ######################## From 2c3ed5acb2c9d51de947812af382fe4f6d17b8f3 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 21 Aug 2025 21:20:18 +0000 Subject: [PATCH 19/20] docs: changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ea4dda..540a97e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ### Chores + Dependencies updated, [ced885e0128b6d5d3a3c7cb97d7e53bc2da64893], [f9b40ea03d0e70e235c28646ff3f9ebb468a904d] + Rust 1.89 linting, [79d19ceeb81ae60bc5562683e405d6e74e6f2578] ++ GitHub workflow updated, [08384200558fa1b9d378ea62ea832708caebaa91], [6573af1ed7d382a81c1305397e904066bb8395a8] ### Features + Horizontally scroll across logs. By default use `←` & `→` keys to traverse horizontally across the lines when logs panel selected. Updated `config.toml` with `log_scroll_forward` and `log_scroll_back` [c190f0206cc55b8e45b8373f9be954e828c18b3b], [8939ac0345326633e794cc10a981a1f3c5c07549] From 423e1af763492c675692f05f18823154378c5833 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 21 Aug 2025 21:48:16 +0000 Subject: [PATCH 20/20] chore: release v0.11.0 --- .github/release-body.md | 16 +++++++++++++--- CHANGELOG.md | 17 ++++++++++------- Cargo.lock | 2 +- Cargo.toml | 2 +- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/.github/release-body.md b/.github/release-body.md index c3b48a8..f17bc3a 100644 --- a/.github/release-body.md +++ b/.github/release-body.md @@ -1,6 +1,16 @@ -### 2025-06-19 +### 2025-08-21 -### Reverts -+ Bollard update rolled back, closes #66, [aac9c6b598ce6c23b14f5a8b0116e662b18074d2] +### Chores ++ Dependencies updated, [ced885e0128b6d5d3a3c7cb97d7e53bc2da64893], [f9b40ea03d0e70e235c28646ff3f9ebb468a904d] ++ Rust 1.89 linting, [79d19ceeb81ae60bc5562683e405d6e74e6f2578] ++ GitHub workflow updated, [08384200558fa1b9d378ea62ea832708caebaa91], [6573af1ed7d382a81c1305397e904066bb8395a8] + +### Features ++ Horizontally scroll across logs. By default use `←` & `→` keys to traverse horizontally across the lines when logs panel selected. Updated `config.toml` with `log_scroll_forward` and `log_scroll_back` [c190f0206cc55b8e45b8373f9be954e828c18b3b], [8939ac0345326633e794cc10a981a1f3c5c07549] ++ Force clear screen & redraw of UI. By default uses `f` key, `config.toml` updated with `force_redraw` [50edbc0cc09db864835fe81a03cba8eadafe548b] ++ Increase scroll speed using the `ctrl` key in conjuction with a scroll key, `config.toml` updated with `scroll_modifier`. The next release will remove `scroll_down_many` & `scroll_down_up` keys, [c5bbffdb5f9e800951e4060aa6aee8e00db589aa] + +### Refactors ++ remove macos cfg none-const functions, Zigbuild now uses Rust 1.87.0, [eb686e2c952e04da74b3e12c0bfa015ec4615e1d] see CHANGELOG.md for more details diff --git a/CHANGELOG.md b/CHANGELOG.md index 540a97e..d17d0fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,18 @@ +# v0.11.0 +### 2025-08-21 + ### Chores -+ Dependencies updated, [ced885e0128b6d5d3a3c7cb97d7e53bc2da64893], [f9b40ea03d0e70e235c28646ff3f9ebb468a904d] -+ Rust 1.89 linting, [79d19ceeb81ae60bc5562683e405d6e74e6f2578] -+ GitHub workflow updated, [08384200558fa1b9d378ea62ea832708caebaa91], [6573af1ed7d382a81c1305397e904066bb8395a8] ++ Dependencies updated, [ced885e0](https://github.com/mrjackwills/oxker/commit/ced885e0128b6d5d3a3c7cb97d7e53bc2da64893), [f9b40ea0](https://github.com/mrjackwills/oxker/commit/f9b40ea03d0e70e235c28646ff3f9ebb468a904d) ++ Rust 1.89 linting, [79d19cee](https://github.com/mrjackwills/oxker/commit/79d19ceeb81ae60bc5562683e405d6e74e6f2578) ++ GitHub workflow updated, [08384200](https://github.com/mrjackwills/oxker/commit/08384200558fa1b9d378ea62ea832708caebaa91), [6573af1e](https://github.com/mrjackwills/oxker/commit/6573af1ed7d382a81c1305397e904066bb8395a8) ### Features -+ Horizontally scroll across logs. By default use `←` & `→` keys to traverse horizontally across the lines when logs panel selected. Updated `config.toml` with `log_scroll_forward` and `log_scroll_back` [c190f0206cc55b8e45b8373f9be954e828c18b3b], [8939ac0345326633e794cc10a981a1f3c5c07549] -+ Force clear screen & redraw of UI. By default uses `f` key, `config.toml` updated with `force_redraw` [50edbc0cc09db864835fe81a03cba8eadafe548b] -+ Increase scroll speed using the `ctrl` key in conjuction with a scroll key, `config.toml` updated with `scroll_modifier`. The next release will remove `scroll_down_many` & `scroll_down_up` keys, [c5bbffdb5f9e800951e4060aa6aee8e00db589aa] ++ Horizontally scroll across logs. By default use `←` & `→` keys to traverse horizontally across the lines when logs panel selected. Updated `config.toml` with `log_scroll_forward` and `log_scroll_back` [c190f020](https://github.com/mrjackwills/oxker/commit/c190f0206cc55b8e45b8373f9be954e828c18b3b), [8939ac03](https://github.com/mrjackwills/oxker/commit/8939ac0345326633e794cc10a981a1f3c5c07549) ++ Force clear screen & redraw of UI. By default uses `f` key, `config.toml` updated with `force_redraw` [50edbc0c](https://github.com/mrjackwills/oxker/commit/50edbc0cc09db864835fe81a03cba8eadafe548b) ++ Increase scroll speed using the `ctrl` key in conjuction with a scroll key, `config.toml` updated with `scroll_modifier`. The next release will remove `scroll_down_many` & `scroll_down_up` keys, [c5bbffdb](https://github.com/mrjackwills/oxker/commit/c5bbffdb5f9e800951e4060aa6aee8e00db589aa) ### Refactors -+ remove macos cfg none-const functions, Zigbuild now uses Rust 1.87.0, [eb686e2c952e04da74b3e12c0bfa015ec4615e1d] ++ remove macos cfg none-const functions, Zigbuild now uses Rust 1.87.0, [eb686e2c](https://github.com/mrjackwills/oxker/commit/eb686e2c952e04da74b3e12c0bfa015ec4615e1d) # v0.10.5 ### 2025-06-19 diff --git a/Cargo.lock b/Cargo.lock index f094380..7d2cf58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1193,7 +1193,7 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "oxker" -version = "0.10.5" +version = "0.11.0" dependencies = [ "anyhow", "bollard", diff --git a/Cargo.toml b/Cargo.toml index 5e72211..83e4ff0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxker" -version = "0.10.5" +version = "0.11.0" edition = "2024" authors = ["Jack Wills "] description = "A simple tui to view & control docker containers"