refactor: redesigned help panel

This commit is contained in:
Jack Wills
2026-02-06 15:33:22 +00:00
parent bebb687c59
commit ae7f3f4a94
45 changed files with 3209 additions and 1160 deletions
Generated
+1
View File
@@ -1890,6 +1890,7 @@ version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"indexmap 2.13.0",
"itoa",
"memchr",
"serde",
+2 -1
View File
@@ -36,7 +36,7 @@ jiff = { version = "0.2", features = ["tzdb-bundle-always"] }
parking_lot = { version = "0.12" }
ratatui = "0.30"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_json = { version = "1.0"}
serde_jsonc = "1.0"
tokio = { version = "1.49", features = ["full"] }
tokio-util = "0.7"
@@ -47,6 +47,7 @@ uuid = { version = "1.20", features = ["fast-rng", "v4"] }
[dev-dependencies]
insta = "1.46"
serde_json = { version = "1.0", features = ["preserve_order"]}
[profile.release]
lto = true
+6 -2
View File
@@ -104,10 +104,10 @@
"k"
],
// Horizontal scroll of the logs
"log_scroll_forward": [
"scroll_forward": [
"right"
],
"log_scroll_back": [
"scroll_back": [
"left"
],
// Select next panel
@@ -170,6 +170,10 @@
"log_section_toggle": [
"\\"
],
// Toggle to inspect container screen
"inspect": [
"i"
],
// Force a complete clear & redraw of the screen
"force_redraw": [
"f"
+4 -2
View File
@@ -95,8 +95,8 @@ scroll_start = ["home"]
# scroll up a list by one item
scroll_up = ["up", "k"]
# Horizontal scroll of the logs
log_scroll_forward = ["right"]
log_scroll_back = ["left"]
scroll_forward = ["right"]
scroll_back = ["left"]
# Select next panel
select_next_panel = ["tab"]
# Select previous panel
@@ -122,6 +122,8 @@ log_section_height_decrease = ["-"]
log_section_height_increase = ["+"]
# Toggle visibility of the log section
log_section_toggle = ["\\"]
# Toggle to inspect container screen
inspect = ["i"]
+26 -14
View File
@@ -24,8 +24,12 @@ const ONE_GB: f64 = ONE_MB * 1000.0;
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
pub enum ScrollDirection {
Next,
Previous,
// Next,
// Previous,
Up,
Down,
Left,
Right,
}
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
@@ -185,8 +189,10 @@ impl<T> StatefulList<T> {
pub fn scroll(&mut self, scroll: &ScrollDirection) {
match scroll {
ScrollDirection::Next => self.next(),
ScrollDirection::Previous => self.previous(),
ScrollDirection::Down => self.next(),
ScrollDirection::Up => self.previous(),
// TODO set offset
_ => (),
}
}
@@ -615,7 +621,8 @@ pub struct Logs {
tz: HashSet<LogsTz>,
search_results: Vec<usize>,
search_term: Option<String>,
offset: u16,
offset: usize,
max_offset: usize,
max_log_len: usize,
adjusted_max_width: usize,
adjust_max_width_text_len: usize,
@@ -629,6 +636,7 @@ impl Default for Logs {
lines,
tz: HashSet::new(),
offset: 0,
max_offset: 0,
search_term: None,
search_results: vec![],
adjusted_max_width: 0,
@@ -687,8 +695,10 @@ impl Logs {
.position(|i| i == &current_selected)
{
if let Some(new_index) = match sd {
ScrollDirection::Next => current_position.checked_add(1),
ScrollDirection::Previous => current_position.checked_sub(1),
ScrollDirection::Down => current_position.checked_add(1),
ScrollDirection::Up => current_position.checked_sub(1),
// TODO set offset
_ => None,
} && let Some(f) = self.search_results.get(new_index)
{
self.lines.state.select(Some(*f));
@@ -696,13 +706,15 @@ impl Logs {
}
} else {
let range = match sd {
ScrollDirection::Previous => (0..=current_selected).rev().collect::<Vec<_>>(),
ScrollDirection::Next => (current_selected
ScrollDirection::Up => (0..=current_selected).rev().collect::<Vec<_>>(),
ScrollDirection::Down => (current_selected
..=self
.search_results
.last()
.map_or_else(|| current_selected, |i| *i))
.collect::<Vec<_>>(),
// TODO set offset
_ => vec![],
};
for i in range {
if self.search_results.contains(&i) {
@@ -820,7 +832,7 @@ impl Logs {
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 {
let arrow_right = if self.offset < self.adjusted_max_width {
""
} else {
" "
@@ -883,10 +895,10 @@ impl Logs {
pub fn get_visible_logs(&self, size: Size, padding: usize) -> Vec<Text<'static>> {
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 {
let char_offset = if self.offset > self.max_log_len {
self.max_log_len
} else {
self.offset.into()
self.offset
};
self.lines
@@ -920,10 +932,10 @@ impl Logs {
/// Add a padding so one char will always be visilbe?
pub fn forward(&mut self, width: u16) {
let offset = usize::from(self.offset);
// Need to set a max_offset, instead of using a width each time
if self.horizontal_scroll_able(width)
&& self.adjusted_max_width > 0
&& offset < self.adjusted_max_width
&& self.offset < self.adjusted_max_width
{
self.offset = self.offset.saturating_add(1);
}
+85 -25
View File
@@ -1,4 +1,4 @@
use bollard::models::ContainerSummary;
use bollard::{models::ContainerSummary, secret::ContainerInspectResponse};
use core::fmt;
use parking_lot::Mutex;
use ratatui::{layout::Size, text::Text, widgets::ListState};
@@ -114,6 +114,45 @@ impl Filter {
}
}
#[derive(Debug, Clone)]
pub struct InspectData {
pub width: usize,
pub height: usize,
pub as_string: String,
pub name: String,
pub id: ContainerId, // pub as_lines: Vec<Line<'a>>,
}
impl From<ContainerInspectResponse> for InspectData {
fn from(input: ContainerInspectResponse) -> Self {
let as_string = serde_json::to_string_pretty(&input)
.unwrap_or_default()
.lines()
.skip(1)
.collect::<Vec<_>>()
.split_last()
.map(|(_, data)| data)
.unwrap_or_default()
.join("\n");
let height = as_string.lines().count();
let mut width = 0;
for i in as_string.lines() {
width = width.max(i.chars().count());
}
Self {
name: input.name.unwrap_or_default(),
// TODO maybe make this an Option<Id>?
id: ContainerId::from(input.id.unwrap_or_default().as_str()),
width,
height,
as_string,
}
}
}
/// Global app_state, stored in an Arc<Mutex>
#[derive(Debug, Clone)]
#[cfg(not(test))]
@@ -122,6 +161,7 @@ pub struct AppData {
error: Option<AppError>,
filter: Filter,
hidden_containers: Vec<ContainerItem>,
inspect_data: Option<InspectData>,
rerender: Arc<Rerender>,
sorted_by: Option<(Header, SortedOrder)>,
current_sorted_id: Vec<ContainerId>,
@@ -136,6 +176,7 @@ pub struct AppData {
pub error: Option<AppError>,
pub filter: Filter,
pub hidden_containers: Vec<ContainerItem>,
pub inspect_data: Option<InspectData>,
pub current_sorted_id: Vec<ContainerId>,
pub rerender: Arc<Rerender>,
pub sorted_by: Option<(Header, SortedOrder)>,
@@ -151,6 +192,7 @@ impl AppData {
error: None,
filter: Filter::new(),
hidden_containers: vec![],
inspect_data: None,
rerender: Arc::clone(redraw),
sorted_by: None,
}
@@ -165,6 +207,18 @@ impl AppData {
.as_secs()
}
pub fn clear_inspect_data(&mut self) {
self.inspect_data = None;
}
pub fn set_inspect_data(&mut self, data: ContainerInspectResponse) {
self.inspect_data = Some(InspectData::from(data))
// self.inspect_data = Some(data)
}
pub fn get_inspect_data(&self) -> Option<InspectData> {
self.inspect_data.clone()
}
/// Filter related methods
/// Get the filterby and filter_term
pub const fn get_filter(&self) -> (FilterBy, Option<&String>) {
@@ -329,6 +383,7 @@ impl AppData {
.iter()
.position(|i| self.get_selected_container_id().as_ref() == Some(&i.id)),
);
self.rerender.update_draw();
}
/// Remove the sorted header & order, and sort by default - created datetime
@@ -667,19 +722,22 @@ impl AppData {
}
pub fn logs_horizontal_scroll(&mut self, sd: &ScrollDirection, width: u16) {
// Change this to set a max_offset, instead of taking in width each time, then can be combined with the log_scroll beneath
match sd {
ScrollDirection::Next => {
ScrollDirection::Down => {
if let Some(i) = self.get_mut_selected_container() {
i.logs.forward(width);
self.rerender.update_draw();
}
}
ScrollDirection::Previous => {
ScrollDirection::Up => {
if let Some(i) = self.get_mut_selected_container() {
i.logs.back();
self.rerender.update_draw();
}
}
// TODO set offset
_ => (),
}
}
@@ -687,8 +745,10 @@ impl AppData {
pub fn log_scroll(&mut self, scroll: &ScrollDirection) {
if let Some(i) = self.get_mut_selected_container() {
match scroll {
ScrollDirection::Next => i.logs.next(),
ScrollDirection::Previous => i.logs.previous(),
ScrollDirection::Down => i.logs.next(),
ScrollDirection::Up => i.logs.previous(),
// TODO set offset
_ => (),
}
self.rerender.update_draw();
}
@@ -876,7 +936,7 @@ impl AppData {
// If removed container is currently selected, then change selected to previous
// This will default to 0 in any edge cases
if self.containers.state.selected().is_some() {
self.containers.scroll(&ScrollDirection::Previous);
self.containers.scroll(&ScrollDirection::Up);
}
// Check is some, else can cause out of bounds error, if containers get removed before a docker update
if self.containers.items.get(index).is_some() {
@@ -1443,7 +1503,7 @@ mod tests {
);
// Calling previous when at start has no effect
app_data.containers_scroll(&ScrollDirection::Previous);
app_data.containers_scroll(&ScrollDirection::Up);
let result = app_data.get_selected_container_id();
assert_eq!(result, Some(ContainerId::from("1")));
let result = app_data.get_selected_container_id_state_name();
@@ -1466,7 +1526,7 @@ mod tests {
// Advance list state by 1
app_data.containers_start();
app_data.containers.scroll(&ScrollDirection::Next);
app_data.containers.scroll(&ScrollDirection::Down);
let result = app_data.get_container_state();
assert_eq!(result.selected(), Some(1));
@@ -1510,7 +1570,7 @@ mod tests {
);
// Calling previous when at end has no effect
app_data.containers.scroll(&ScrollDirection::Next);
app_data.containers.scroll(&ScrollDirection::Down);
let result = app_data.get_selected_container_id();
assert_eq!(result, Some(ContainerId::from("3")));
let result = app_data.get_selected_container_id_state_name();
@@ -1531,7 +1591,7 @@ mod tests {
let mut app_data = gen_appdata(&containers);
app_data.containers_end();
app_data.containers.scroll(&ScrollDirection::Previous);
app_data.containers.scroll(&ScrollDirection::Up);
let result = app_data.get_container_state();
assert_eq!(result.selected(), Some(1));
assert_eq!(result.offset(), 0);
@@ -1547,7 +1607,7 @@ mod tests {
assert_eq!(result, None);
app_data.containers.start();
app_data.containers.scroll(&ScrollDirection::Next);
app_data.containers.scroll(&ScrollDirection::Down);
let result = app_data.get_selected_container();
assert_eq!(result, Some(&containers[1]));
@@ -1634,7 +1694,7 @@ mod tests {
let mut app_data = gen_appdata(&containers);
app_data.containers_start();
app_data.docker_controls_start();
app_data.docker_controls_scroll(&ScrollDirection::Next);
app_data.docker_controls_scroll(&ScrollDirection::Down);
let result = app_data.selected_docker_controls();
assert_eq!(result, Some(DockerCommand::Restart));
@@ -1652,7 +1712,7 @@ mod tests {
assert_eq!(result, Some(DockerCommand::Delete));
// Next has no effect when at end
app_data.docker_controls_scroll(&ScrollDirection::Next);
app_data.docker_controls_scroll(&ScrollDirection::Down);
let result = app_data.selected_docker_controls();
assert_eq!(result, Some(DockerCommand::Delete));
}
@@ -1664,14 +1724,14 @@ mod tests {
let mut app_data = gen_appdata(&containers);
app_data.containers_start();
app_data.docker_controls_end();
app_data.docker_controls_scroll(&ScrollDirection::Previous);
app_data.docker_controls_scroll(&ScrollDirection::Up);
let result = app_data.selected_docker_controls();
assert_eq!(result, Some(DockerCommand::Stop));
// previous has no effect when at start
app_data.docker_controls_start();
app_data.docker_controls_scroll(&ScrollDirection::Previous);
app_data.docker_controls_scroll(&ScrollDirection::Up);
let result = app_data.selected_docker_controls();
assert_eq!(result, Some(DockerCommand::Pause));
}
@@ -1935,7 +1995,7 @@ mod tests {
assert_eq!(result, " 3/3 - container_1 - image_1");
// Change log state to no longer be at the end
app_data.log_scroll(&ScrollDirection::Previous);
app_data.log_scroll(&ScrollDirection::Up);
let result = app_data.get_log_title();
assert_eq!(result, " 2/3 - container_1 - image_1");
}
@@ -1956,7 +2016,7 @@ mod tests {
assert_eq!(result, " - container_1 - image_1");
// change container
app_data.containers_scroll(&ScrollDirection::Next);
app_data.containers_scroll(&ScrollDirection::Down);
let result = app_data.get_log_title();
assert_eq!(result, " - container_2 - image_2");
@@ -1967,7 +2027,7 @@ mod tests {
assert_eq!(result, " 3/3 - container_2 - image_2");
// Change log state to no longer be at the end
app_data.log_scroll(&ScrollDirection::Previous);
app_data.log_scroll(&ScrollDirection::Up);
let result = app_data.get_log_title();
assert_eq!(result, " 2/3 - container_2 - image_2");
}
@@ -2074,7 +2134,7 @@ mod tests {
let result = app_data.get_log_title();
assert_eq!(result, " 1/3 - container_1 - image_1");
app_data.log_scroll(&ScrollDirection::Next);
app_data.log_scroll(&ScrollDirection::Down);
let result = app_data.get_log_state();
assert!(result.is_some());
assert_eq!(result.as_ref().unwrap().selected(), Some(1));
@@ -2083,7 +2143,7 @@ mod tests {
let result = app_data.get_log_title();
assert_eq!(result, " 2/3 - container_1 - image_1");
app_data.log_scroll(&ScrollDirection::Next);
app_data.log_scroll(&ScrollDirection::Down);
let result = app_data.get_log_state();
assert!(result.is_some());
assert_eq!(result.as_ref().unwrap().selected(), Some(2));
@@ -2091,7 +2151,7 @@ mod tests {
let result = app_data.get_log_title();
assert_eq!(result, " 3/3 - container_1 - image_1");
app_data.log_scroll(&ScrollDirection::Next);
app_data.log_scroll(&ScrollDirection::Down);
let result = app_data.get_log_state();
assert!(result.is_some());
@@ -2122,7 +2182,7 @@ mod tests {
let result = app_data.get_log_title();
assert_eq!(result, " 3/3 - container_1 - image_1");
app_data.log_scroll(&ScrollDirection::Previous);
app_data.log_scroll(&ScrollDirection::Up);
let result = app_data.get_log_state();
assert!(result.is_some());
@@ -2131,7 +2191,7 @@ mod tests {
let result = app_data.get_log_title();
assert_eq!(result, " 2/3 - container_1 - image_1");
app_data.log_scroll(&ScrollDirection::Previous);
app_data.log_scroll(&ScrollDirection::Up);
let result = app_data.get_log_state();
assert!(result.is_some());
assert_eq!(result.as_ref().unwrap().selected(), Some(0));
@@ -2139,7 +2199,7 @@ mod tests {
let result = app_data.get_log_title();
assert_eq!(result, " 1/3 - container_1 - image_1");
app_data.log_scroll(&ScrollDirection::Previous);
app_data.log_scroll(&ScrollDirection::Up);
let result = app_data.get_log_state();
assert!(result.is_some());
assert_eq!(result.as_ref().unwrap().selected(), Some(0));
@@ -2433,7 +2493,7 @@ mod tests {
}
for _ in 0..=500 {
app_data.log_scroll(&ScrollDirection::Next);
app_data.log_scroll(&ScrollDirection::Down);
}
let result = app_data.get_logs(
Size {
+4 -4
View File
@@ -95,8 +95,8 @@ scroll_start = ["home"]
# scroll up a list by one item
scroll_up = ["up", "k"]
# Horizontal scroll of the logs
log_scroll_forward = ["right"]
log_scroll_back = ["left"]
scroll_forward = ["right"]
scroll_back = ["left"]
# Select next panel
select_next_panel = ["tab"]
# Select previous panel
@@ -122,8 +122,8 @@ log_section_height_decrease = ["-"]
log_section_height_increase = ["+"]
# Toggle visibility of the log section
log_section_toggle = ["\\"]
# Toggle to inspect container screen
inspect = ["i"]
# Force a complete clear & redraw of the screen
force_redraw = ["f"]
+22 -18
View File
@@ -42,8 +42,9 @@ optional_config_struct!(
exec,
filter_mode,
force_redraw,
log_scroll_back,
log_scroll_forward,
inspect,
scroll_back,
scroll_forward,
log_search_mode,
log_section_height_decrease,
log_section_height_increase,
@@ -77,9 +78,10 @@ config_struct!(
delete_deny,
exec,
filter_mode,
inspect,
force_redraw,
log_scroll_back,
log_scroll_forward,
scroll_back,
scroll_forward,
log_search_mode,
log_section_height_decrease,
log_section_height_increase,
@@ -113,10 +115,11 @@ impl Keymap {
delete_confirm: (KeyCode::Char('y'), None),
delete_deny: (KeyCode::Char('n'), None),
exec: (KeyCode::Char('e'), None),
inspect: (KeyCode::Char('i'), None),
filter_mode: (KeyCode::Char('/'), Some(KeyCode::F(1))),
force_redraw: (KeyCode::Char('f'), None),
log_scroll_back: (KeyCode::Left, None),
log_scroll_forward: (KeyCode::Right, None),
scroll_back: (KeyCode::Left, None),
scroll_forward: (KeyCode::Right, None),
log_search_mode: (KeyCode::Char('#'), None),
log_section_height_decrease: (KeyCode::Char('-'), None),
log_section_height_increase: (KeyCode::Char('='), None),
@@ -206,12 +209,8 @@ impl From<Option<ConfigKeymap>> for Keymap {
update_keymap(ck.scroll_start, &mut keymap.scroll_start, &mut clash);
update_keymap(ck.scroll_up, &mut keymap.scroll_up, &mut clash);
update_keymap(ck.log_search_mode, &mut keymap.log_search_mode, &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.scroll_forward, &mut keymap.scroll_forward, &mut clash);
update_keymap(ck.scroll_back, &mut keymap.scroll_back, &mut clash);
update_keymap(
ck.select_next_panel,
&mut keymap.select_next_panel,
@@ -395,9 +394,10 @@ mod tests {
exec: None,
filter_mode: None,
force_redraw: None,
log_scroll_back: None,
inspect: None,
scroll_back: None,
log_search_mode: None,
log_scroll_forward: None,
scroll_forward: None,
log_section_height_decrease: None,
log_section_height_increase: None,
log_section_toggle: None,
@@ -441,8 +441,10 @@ mod tests {
exec: gen_v(("g", "h")),
filter_mode: gen_v(("i", "j")),
force_redraw: gen_v(("k", "l")),
log_scroll_back: gen_v(("s", "t")),
log_scroll_forward: gen_v(("q", "r")),
// TODO test me
inspect: None,
scroll_back: gen_v(("s", "t")),
scroll_forward: gen_v(("q", "r")),
log_search_mode: gen_v(("1", "2")),
log_section_height_decrease: gen_v(("m", "n")),
log_section_height_increase: gen_v(("o", "p")),
@@ -479,8 +481,10 @@ mod tests {
exec: (KeyCode::Char('g'), Some(KeyCode::Char('h'))),
filter_mode: (KeyCode::Char('i'), Some(KeyCode::Char('j'))),
force_redraw: (KeyCode::Char('k'), Some(KeyCode::Char('l'))),
log_scroll_back: (KeyCode::Char('s'), Some(KeyCode::Char('t'))),
log_scroll_forward: (KeyCode::Char('q'), Some(KeyCode::Char('r'))),
//todo test me
inspect: (KeyCode::Char('i'), None),
scroll_back: (KeyCode::Char('s'), Some(KeyCode::Char('t'))),
scroll_forward: (KeyCode::Char('q'), Some(KeyCode::Char('r'))),
log_search_mode: (KeyCode::Char('1'), Some(KeyCode::Char('2'))),
log_section_height_decrease: (KeyCode::Char('m'), Some(KeyCode::Char('n'))),
log_section_height_increase: (KeyCode::Char('o'), Some(KeyCode::Char('p'))),
+16 -12
View File
@@ -25,7 +25,8 @@ pub struct Config {
pub keymap: Keymap,
pub log_search_case_sensitive: bool,
pub raw_logs: bool,
pub save_dir: Option<PathBuf>,
pub dir_config: Option<PathBuf>,
pub dir_save: Option<PathBuf>,
pub show_logs: bool,
pub show_self: bool,
pub show_std_err: bool,
@@ -47,7 +48,8 @@ impl From<&Args> for Config {
keymap: Keymap::new(),
log_search_case_sensitive: true,
raw_logs: args.raw,
save_dir: Self::try_get_logs_dir(args.save_dir.as_ref()),
dir_save: Self::try_get_logs_dir(args.save_dir.as_ref()),
dir_config: args.config_file.as_ref().map(|i| PathBuf::from(&i)),
show_logs: true,
show_self: !args.show_self,
show_std_err: !args.no_std_err,
@@ -59,19 +61,20 @@ impl From<&Args> for Config {
}
}
impl From<ConfigFile> for Config {
fn from(config_file: ConfigFile) -> Self {
impl From<(ConfigFile, Option<PathBuf>)> for Config {
fn from((config_file, dir): (ConfigFile, Option<PathBuf>)) -> Self {
Self {
app_colors: AppColors::from(config_file.colors),
color_logs: config_file.color_logs.unwrap_or(false),
docker_interval_ms: config_file.docker_interval.unwrap_or(1000),
dir_config: dir,
gui: config_file.gui.unwrap_or(true),
host: config_file.host,
in_container: Self::check_if_in_container(),
keymap: Keymap::from(config_file.keymap),
log_search_case_sensitive: config_file.log_search_case_sensitive.unwrap_or(true),
raw_logs: config_file.raw_logs.unwrap_or(false),
save_dir: Self::try_get_logs_dir(config_file.save_dir.as_ref()),
dir_save: Self::try_get_logs_dir(config_file.save_dir.as_ref()),
show_logs: config_file.show_logs.unwrap_or(true),
show_self: config_file.show_self.unwrap_or(false),
show_std_err: config_file.show_std_err.unwrap_or(true),
@@ -182,8 +185,8 @@ impl Config {
self.host = Some(host);
}
if let Some(x) = config_from_cli.save_dir {
self.save_dir = Some(x);
if let Some(x) = config_from_cli.dir_save {
self.dir_save = Some(x);
}
if let Some(tz) = config_from_cli.timezone {
@@ -208,15 +211,16 @@ impl Config {
let args = Args::parse();
let config_from_cli = Self::from(&args);
if let Some(config_file) = &args.config_file
if let Some(dir_config_file) = &args.config_file
&& let Some(config_file) =
parse_config_file::ConfigFile::try_parse_from_file(config_file)
parse_config_file::ConfigFile::try_parse_from_file(dir_config_file)
{
return Self::from(config_file).merge_args(config_from_cli);
return Self::from((config_file, Some(PathBuf::from(dir_config_file))))
.merge_args(config_from_cli);
}
if let Some(config_file) = parse_config_file::ConfigFile::try_parse(in_container) {
return Self::from(config_file).merge_args(config_from_cli);
if let Some((config_file, dir)) = parse_config_file::ConfigFile::try_parse(in_container) {
return Self::from((config_file, Some(dir))).merge_args(config_from_cli);
}
config_from_cli
}
+9 -13
View File
@@ -81,7 +81,7 @@ pub struct ConfigFile {
impl ConfigFile {
/// Attempt to create a config.toml file, will attempt to recursively create the directories as well
fn crate_config_file(in_container: bool) -> Result<(), AppError> {
fn create_config_file(in_container: bool) -> Result<(), AppError> {
if in_container {
return Ok(());
}
@@ -119,8 +119,6 @@ impl ConfigFile {
toml::from_str::<Self>(input).map_err(|i| AppError::Parse(i.message().to_owned()))
}
}
// TODO if on windows, omit the docker_host?
}
/// Read the config file path to string, then attempt to parse
@@ -148,28 +146,26 @@ impl ConfigFile {
/// Parse a config file using default config_file location
/// This is executed first, then the CLI args are read, and if they contain a "--config-file" entry, then Self::try_parse_from_file() is executed
pub fn try_parse(in_container: bool) -> Option<Self> {
let mut config = None;
pub fn try_parse(in_container: bool) -> Option<(Self, PathBuf)> {
let mut output = None;
for file_format in [
ConfigFileFormat::Toml,
ConfigFileFormat::Jsonc,
ConfigFileFormat::JsoncAsJson,
ConfigFileFormat::Json,
] {
if let Ok(config_file) = Self::parse_config_file(
file_format,
&file_format.get_default_path_name(in_container),
) {
config = Some(config_file);
let path = file_format.get_default_path_name(in_container);
if let Ok(config_file) = Self::parse_config_file(file_format, &path) {
output = Some((config_file, path));
break;
}
}
if config.is_none() {
Self::crate_config_file(in_container).ok();
if output.is_none() {
Self::create_config_file(in_container).ok();
}
config
output
}
}
+1
View File
@@ -9,5 +9,6 @@ pub enum DockerMessage {
ConfirmDelete(ContainerId),
Control((DockerCommand, ContainerId)),
Exec(Sender<Arc<Docker>>),
Inspect(ContainerId),
Update,
}
+14 -2
View File
@@ -1,8 +1,8 @@
use bollard::{
Docker,
query_parameters::{
ListContainersOptions, LogsOptions, RemoveContainerOptions, RestartContainerOptions,
StartContainerOptions, StatsOptions, StopContainerOptions,
InspectContainerOptions, ListContainersOptions, LogsOptions, RemoveContainerOptions,
RestartContainerOptions, StartContainerOptions, StatsOptions, StopContainerOptions,
},
secret::ContainerStatsResponse,
service::ContainerSummary,
@@ -413,6 +413,18 @@ impl DockerData {
docker_tx.send(Arc::clone(&self.docker)).ok();
}
DockerMessage::Update => self.update_everything().await,
DockerMessage::Inspect(id) => {
let t = self
.docker
.inspect_container(id.get(), Some(InspectContainerOptions { size: true }))
.await;
if let Ok(t) = t {
self.app_data.lock().set_inspect_data(t);
self.gui_state.lock().status_push(Status::Inspect);
} else {
// Set error here, can't inspect container
}
}
}
}
}
+26 -20
View File
@@ -1,5 +1,5 @@
use std::{
io::{Read, Stdout, Write},
io::{Read, Write},
sync::{Arc, atomic::AtomicBool, mpsc::Sender},
};
@@ -10,7 +10,7 @@ use bollard::{
use crossterm::terminal::enable_raw_mode;
use futures_util::StreamExt;
use parking_lot::Mutex;
use ratatui::{Terminal, backend::CrosstermBackend};
use ratatui::layout::Size;
use tokio::{
fs::File,
io::{AsyncReadExt, AsyncWriteExt},
@@ -123,23 +123,29 @@ impl AsyncTTY {
}
}
/// This is used to set the terminal size when exec via the Internal method
#[derive(Debug, Clone)]
pub struct TerminalSize {
width: u16,
height: u16,
}
// impl TryFrom<&Terminal<CrosstermBackend<Stdout>>> for HWU16 {
// type Error = None;
// fn try_from(terminal: &Terminal<CrosstermBackend<Stdout>>) -> Option<Self> {
// terminal.size().map_or(None, |i| {
// Some(Self {
// width: i.width,
// height: i.height,
// })
// })
// }
impl TerminalSize {
pub fn new(terminal: &Terminal<CrosstermBackend<Stdout>>) -> Option<Self> {
terminal.size().map_or(None, |i| {
Some(Self {
width: i.width,
height: i.height,
})
})
}
}
// }
// impl TerminalSize {
// pub fn new(terminal: &Terminal<CrosstermBackend<Stdout>>) -> Option<Self> {
// terminal.size().map_or(None, |i| {
// Some(Self {
// width: i.width,
// height: i.height,
// })
// })
// }
// }
#[derive(Debug, Clone)]
pub enum ExecMode {
@@ -225,7 +231,7 @@ impl ExecMode {
&self,
id: &ContainerId,
docker: &Arc<Docker>,
terminal_size: Option<TerminalSize>,
terminal_size: Option<Size>,
) -> Result<(), AppError> {
let cancel_token = CancellationToken::new();
@@ -341,7 +347,7 @@ impl ExecMode {
}
}
pub async fn run(&self, tty_size: Option<TerminalSize>) -> Result<(), AppError> {
pub async fn run(&self, tty_size: Option<Size>) -> Result<(), AppError> {
match self {
Self::External(id) => {
Self::exec_external(id);
+115 -23
View File
@@ -119,6 +119,14 @@ impl InputHandler {
self.gui_state.lock().set_delete_container(None);
}
async fn inspect_key(&self) {
self.app_data.lock().clear_inspect_data();
let selected = self.app_data.lock().get_selected_container().cloned();
if let Some(g) = selected {
self.docker_tx.send(DockerMessage::Inspect(g.id)).await.ok();
}
}
/// Validate that one can exec into a Docker container
async fn exec_key(&self) {
let is_oxker = self.app_data.lock().is_oxker();
@@ -179,7 +187,7 @@ impl InputHandler {
let args = self.app_data.lock().config.clone();
let container = self.app_data.lock().get_selected_container_id_state_name();
if let Some((id, _, name)) = container
&& let Some(log_path) = args.save_dir
&& let Some(log_path) = args.dir_save
{
let (sx, rx) = tokio::sync::oneshot::channel();
self.docker_tx.send(DockerMessage::Exec(sx)).await?;
@@ -296,6 +304,18 @@ impl InputHandler {
}
}
fn inspect_scroll(&self, modifier: KeyModifiers, sd: &ScrollDirection) {
for _ in 0..self.get_modifier_total(modifier) {
self.gui_state.lock().set_inspect_offset(sd);
}
}
// fn inspect_scroll(&self, modifier: KeyModifiers, sd: &ScrollDirection) {
// for _ in 0..self.get_modifier_total(modifier) {
// self.gui_state.lock().set_inspect_offset(sd);
// }
// }
fn logs_horizontal_scroll(&self, modifier: KeyModifiers, sd: &ScrollDirection) {
let panel = self.gui_state.lock().get_selected_panel();
if panel == SelectablePanel::Logs {
@@ -393,22 +413,22 @@ impl InputHandler {
self.gui_state.lock().status_del(Status::SearchLogs);
}
_ if self.keymap.log_scroll_back.0 == key_code
|| self.keymap.log_scroll_back.1 == Some(key_code) =>
_ if self.keymap.scroll_back.0 == key_code
|| self.keymap.scroll_back.1 == Some(key_code) =>
{
self.logs_horizontal_scroll(modifier, &ScrollDirection::Previous);
self.logs_horizontal_scroll(modifier, &ScrollDirection::Up);
}
_ if self.keymap.log_scroll_forward.0 == key_code
|| self.keymap.log_scroll_forward.1 == Some(key_code) =>
_ if self.keymap.scroll_forward.0 == key_code
|| self.keymap.scroll_forward.1 == Some(key_code) =>
{
self.logs_horizontal_scroll(modifier, &ScrollDirection::Next);
self.logs_horizontal_scroll(modifier, &ScrollDirection::Down);
}
_ if self.keymap.scroll_down.0 == key_code => {
self.app_data
.lock()
.log_search_scroll(&ScrollDirection::Next);
.log_search_scroll(&ScrollDirection::Down);
// TODO should only do this is log_search_scroll returns some
// Need to wait til app_data and gui_data is combined
self.gui_state
@@ -418,9 +438,7 @@ impl InputHandler {
}
_ if self.keymap.scroll_up.0 == key_code => {
self.app_data
.lock()
.log_search_scroll(&ScrollDirection::Previous);
self.app_data.lock().log_search_scroll(&ScrollDirection::Up);
// TODO should only do this is log_search_scroll returns some
// Need to wait til app_data and gui_data is combined
self.gui_state
@@ -439,6 +457,62 @@ impl InputHandler {
}
}
/// Actions to take when Filter status active
fn handle_inspect(&mut self, key_code: KeyCode, modifier: KeyModifiers) {
match key_code {
_ if self.keymap.inspect.0 == key_code
|| self.keymap.inspect.1 == Some(key_code)
|| self.keymap.clear.0 == key_code
|| self.keymap.clear.1 == Some(key_code) =>
{
self.app_data.lock().clear_inspect_data();
self.gui_state.lock().clear_inspect_offset();
self.gui_state.lock().status_del(Status::Inspect);
}
_ if self.keymap.scroll_down.0 == key_code
|| self.keymap.scroll_down.1 == Some(key_code) =>
{
self.inspect_scroll(modifier, &ScrollDirection::Down);
}
_ if self.keymap.scroll_up.0 == key_code
|| self.keymap.scroll_up.1 == Some(key_code) =>
{
self.inspect_scroll(modifier, &ScrollDirection::Up);
}
_ if self.keymap.scroll_forward.0 == key_code
|| self.keymap.scroll_forward.1 == Some(key_code) =>
{
self.inspect_scroll(modifier, &ScrollDirection::Right);
}
_ if self.keymap.scroll_back.0 == key_code
|| self.keymap.scroll_back.1 == Some(key_code) =>
{
self.inspect_scroll(modifier, &ScrollDirection::Left);
}
_ if self.keymap.toggle_mouse_capture.0 == key_code
|| self.keymap.toggle_mouse_capture.1 == Some(key_code) =>
{
self.mouse_capture_key();
}
_ if self.keymap.scroll_start.0 == key_code
|| self.keymap.scroll_start.1 == Some(key_code) =>
{
self.gui_state.lock().clear_inspect_offset();
}
_ if self.keymap.scroll_end.0 == key_code
|| self.keymap.scroll_end.1 == Some(key_code) =>
{
self.gui_state.lock().set_inspect_offset_y_to_max();
}
_ => (),
}
}
/// Actions to take when Filter status active
fn handle_filter(&self, key_code: KeyCode) {
match key_code {
@@ -596,6 +670,10 @@ impl InputHandler {
self.save_key().await;
}
_ if self.keymap.inspect.0 == key_code || self.keymap.inspect.1 == Some(key_code) => {
self.inspect_key().await;
}
_ if self.keymap.select_next_panel.0 == key_code
|| self.keymap.select_next_panel.1 == Some(key_code) =>
{
@@ -623,13 +701,13 @@ impl InputHandler {
_ if self.keymap.scroll_up.0 == key_code
|| self.keymap.scroll_up.1 == Some(key_code) =>
{
self.scroll(modifier, &ScrollDirection::Previous);
self.scroll(modifier, &ScrollDirection::Up);
}
_ if self.keymap.scroll_down.0 == key_code
|| self.keymap.scroll_down.1 == Some(key_code) =>
{
self.scroll(modifier, &ScrollDirection::Next);
self.scroll(modifier, &ScrollDirection::Down);
}
_ if self.keymap.filter_mode.0 == key_code
@@ -648,18 +726,17 @@ impl InputHandler {
self.gui_state.lock().status_push(Status::SearchLogs);
}
_ if self.keymap.log_scroll_back.0 == key_code
|| self.keymap.log_scroll_back.1 == Some(key_code) =>
_ if self.keymap.scroll_back.0 == key_code
|| self.keymap.scroll_back.1 == Some(key_code) =>
{
self.logs_horizontal_scroll(modifier, &ScrollDirection::Previous);
self.logs_horizontal_scroll(modifier, &ScrollDirection::Up);
// self.logs_back(modifier);
}
_ if self.keymap.log_scroll_forward.0 == key_code
|| self.keymap.log_scroll_forward.1 == Some(key_code) =>
_ if self.keymap.scroll_forward.0 == key_code
|| self.keymap.scroll_forward.1 == Some(key_code) =>
{
self.logs_horizontal_scroll(modifier, &ScrollDirection::Next);
// self.logs_forward(modifier);
self.logs_horizontal_scroll(modifier, &ScrollDirection::Down);
}
KeyCode::Enter => self.enter_key().await,
@@ -678,6 +755,7 @@ impl InputHandler {
let contains_filter = contains(Status::Filter);
let contains_delete = contains(Status::DeleteConfirm);
let contains_search_logs = contains(Status::SearchLogs);
let contains_inspect = contains(Status::Inspect);
if !contains_exec {
let is_q = || key_code == self.keymap.quit.0 || Some(key_code) == self.keymap.quit.1;
@@ -698,6 +776,8 @@ impl InputHandler {
self.handle_search_logs(key_code, key_modifier);
} else if contains_delete {
self.handle_delete(key_code).await;
} else if contains_inspect {
self.handle_inspect(key_code, key_modifier);
} else {
self.handle_others(key_code, key_modifier).await;
}
@@ -726,7 +806,18 @@ impl InputHandler {
/// Handle mouse button events
fn mouse_press(&self, mouse_event: MouseEvent, modifier: KeyModifiers) {
let status = self.gui_state.lock().get_status();
if status.contains(&Status::Help) {
if status.contains(&Status::Inspect) {
match mouse_event.kind {
MouseEventKind::ScrollDown => self.inspect_scroll(modifier, &ScrollDirection::Down),
MouseEventKind::ScrollUp => self.inspect_scroll(modifier, &ScrollDirection::Up),
MouseEventKind::ScrollRight => {
self.inspect_scroll(modifier, &ScrollDirection::Right)
}
MouseEventKind::ScrollLeft => self.inspect_scroll(modifier, &ScrollDirection::Left),
_ => (),
}
} else if status.contains(&Status::Help) {
let mouse_point = Rect::new(mouse_event.column, mouse_event.row, 1, 1);
let help_intersect = self.gui_state.lock().get_intersect_help(mouse_point);
if help_intersect {
@@ -734,8 +825,9 @@ impl InputHandler {
}
} else {
match mouse_event.kind {
MouseEventKind::ScrollUp => self.scroll(modifier, &ScrollDirection::Previous),
MouseEventKind::ScrollDown => self.scroll(modifier, &ScrollDirection::Next),
MouseEventKind::ScrollUp => self.scroll(modifier, &ScrollDirection::Up),
MouseEventKind::ScrollDown => self.scroll(modifier, &ScrollDirection::Down),
// TODO left and right for log offsets
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);
+4 -4
View File
@@ -1,6 +1,6 @@
use std::sync::Arc;
use super::RIGHT_ARROW;
use super::SELECT_ARROW;
use crate::{
app_data::AppData,
config::AppColors,
@@ -44,7 +44,7 @@ pub fn draw(
let items = List::new(items)
.block(block)
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_symbol(RIGHT_ARROW);
.highlight_symbol(SELECT_ARROW);
f.render_stateful_widget(items, area, i);
} else {
let paragraph = Paragraph::new("").block(block).alignment(Alignment::Center);
@@ -173,7 +173,7 @@ mod tests {
setup
.app_data
.lock()
.docker_controls_scroll(&ScrollDirection::Next);
.docker_controls_scroll(&ScrollDirection::Down);
setup
.terminal
@@ -370,7 +370,7 @@ mod tests {
setup
.app_data
.lock()
.docker_controls_scroll(&ScrollDirection::Next);
.docker_controls_scroll(&ScrollDirection::Down);
setup
.terminal
+9 -2
View File
@@ -5,7 +5,14 @@ use ratatui::{
text::{Line, Span},
};
use crate::{app_data::FilterBy, config::AppColors, ui::FrameData};
use crate::{
app_data::FilterBy,
config::AppColors,
ui::{
FrameData,
draw_blocks::{LEFT_ARROW, RIGHT_ARROW},
},
};
/// Create the filter_by by spans, coloured dependant on which one is selected
fn filter_by_spans(colors: AppColors, fd: &'_ FrameData) -> [Span<'_>; 4] {
@@ -46,7 +53,7 @@ pub fn draw(area: Rect, colors: AppColors, frame: &mut Frame, fd: &FrameData) {
let mut line = vec![
Span::styled(" Esc ", style_but),
Span::styled(" clear ", style_desc),
Span::styled(" ← by → ", style_but),
Span::styled(format!(" {LEFT_ARROW} by {RIGHT_ARROW} "), style_but),
Span::from(" "),
];
line.extend_from_slice(&filter_by_spans(colors, fd));
+1115 -590
View File
File diff suppressed because it is too large Load Diff
+782
View File
@@ -0,0 +1,782 @@
use std::sync::Arc;
use parking_lot::Mutex;
use ratatui::{
Frame,
layout::Rect,
style::{Style, Stylize},
text::Line,
widgets::{Block, Borders, Paragraph, Wrap},
};
use crate::{
app_data::InspectData,
config::{AppColors, Keymap},
ui::{
GuiState,
draw_blocks::{DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, UP_ARROW},
gui_state::ScrollOffset,
},
};
/// Create a bordered block with a title.
fn title_block<'a>(upper_title: &'a str, lower_title: &'a str, colors: &AppColors) -> Block<'a> {
Block::default()
.borders(Borders::all())
.border_type(ratatui::widgets::BorderType::Rounded)
.border_style(Style::default().fg(colors.borders.selected))
.title(upper_title.bold().into_centered_line())
.title_bottom(lower_title.bold().into_centered_line())
}
/// Create the upper title, with container name, id, and keymap to clear
fn generate_upper_title(data: &InspectData, keymap: &Keymap) -> String {
let mut output = String::from(" inspecting: ");
let name = if data.name.starts_with("/") {
data.name.replacen('/', "", 1)
} else {
data.name.clone()
};
output.push_str(&format!("{} {} ", name, data.id.get_short()));
let mut inspect_key = keymap.inspect.0.to_string();
if let Some(x) = keymap.inspect.1 {
inspect_key.push_str(&format!(" or {x}"));
}
let mut clear_key = keymap.clear.0.to_string();
if let Some(x) = keymap.clear.1 {
clear_key.push_str(&format!(" or {x}"));
}
output.push_str(&format!(" - {clear_key} or {inspect_key} to exit"));
output.push(' ');
output
}
/// Generate the lower title, with the current scroll and the scrolling limits
fn generate_lower_title(length: usize, width: usize, offset: ScrollOffset) -> String {
let length_width = length
.to_string()
.chars()
.count()
.max(offset.y.to_string().chars().count());
let width_width = width
.to_string()
.chars()
.count()
.max(offset.x.to_string().chars().count());
let left_arrow = if offset.x == 0 { " " } else { LEFT_ARROW };
let right_arrow = if offset.x == width { " " } else { RIGHT_ARROW };
let up_arrow = if offset.y == 0 { " " } else { UP_ARROW };
let down_arrow = if offset.y == length { " " } else { DOWN_ARROW };
format!(
" {up_arrow} {:>length_width$}/{:>length_width$} {down_arrow} {left_arrow} {:>width_width$}/{:>width_width$} {right_arrow} ",
offset.y, length, offset.x, width
)
}
/// Generate the Lines, remove lines & chars based on the offset and viewport
fn gen_lines<'a>(data_as_str: &'a str, offset: &ScrollOffset, rect: &Rect) -> Vec<Line<'a>> {
let first_line_index = offset.y.max(0);
let first_char_index = offset.x.max(0);
let last_char_index = usize::from(rect.width.saturating_sub(2));
let take_lines = usize::from(rect.height);
//todo see if log scrolling does this - What?
data_as_str
.lines()
.skip(first_line_index)
.take(take_lines)
.map(|line| {
Line::from(
line.chars()
.skip(first_char_index)
.take(last_char_index)
.collect::<String>(),
)
})
.collect()
}
/// Draw the InspectContainer widget to the entire screen
pub fn draw(
f: &mut Frame,
colors: AppColors,
data: InspectData,
gui_state: &Arc<Mutex<GuiState>>,
keymap: &Keymap,
) {
let rect = f.area();
let offset = gui_state.lock().get_inspect_offset();
// +2 to account for the border
let height = data
.height
.saturating_sub(usize::from(rect.height))
.saturating_add(2);
let width = data
.width
.saturating_sub(usize::from(rect.width))
.saturating_add(2);
let upper_title = generate_upper_title(&data, keymap);
let lower_title = generate_lower_title(height, width, offset);
gui_state.lock().set_inspect_offset_max(ScrollOffset {
x: width,
y: height,
});
let paragraph = Paragraph::new(gen_lines(&data.as_string, &offset, &rect))
.block(title_block(&upper_title, &lower_title, &colors))
.gray()
.left_aligned()
.wrap(Wrap { trim: false });
f.render_widget(paragraph, rect);
}
// TODO TESTS
// Test keymap
// Test colors
// Test offset y & x
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use std::{collections::HashMap, sync::LazyLock};
use crate::{
app_data::InspectData,
config::{AppColors, Keymap},
ui::draw_blocks::tests::{get_result, test_setup},
};
use bollard::secret::{
ContainerConfig, ContainerInspectResponse, ContainerState, ContainerStateStatusEnum,
DriverData, EndpointSettings, HostConfig, HostConfigLogConfig, MountPoint,
MountPointTypeEnum, NetworkSettings, RestartPolicy, RestartPolicyNameEnum,
};
use crossterm::event::KeyCode;
use insta::assert_snapshot;
use ratatui::style::Color;
static INSPECT_DATA: LazyLock<InspectData> =
LazyLock::new(|| InspectData::from(gen_container_inspect_response()));
#[test]
/// Test a inspect container with default settings, keymap, and position
fn test_draw_blocks_inspect_default_valid() {
let mut setup = test_setup(100, 50, true, true);
setup
.terminal
.draw(|f| {
super::draw(
f,
AppColors::new(),
INSPECT_DATA.clone(),
&setup.gui_state,
&Keymap::new(),
);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
// Assert border colors
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) {
(0 | 49, _) | (_, 0 | 99) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::LightCyan);
}
_ => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::Gray);
}
}
}
}
}
#[test]
/// Test a inspect container with custom colors
fn test_draw_blocks_inspect_custom_color() {
let mut setup = test_setup(100, 50, true, true);
let mut colors = AppColors::new();
colors.borders.selected = Color::Red;
setup
.terminal
.draw(|f| {
super::draw(
f,
colors,
INSPECT_DATA.clone(),
&setup.gui_state,
&Keymap::new(),
);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
// Assert custom border colors
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) {
(0 | 49, _) | (_, 0 | 99) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::Red);
}
_ => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::Gray);
}
}
}
}
}
#[test]
/// Test a inspect container with custom keymap for one clear key
fn test_draw_blocks_inspect_custom_keymap_clear_one() {
let mut setup = test_setup(100, 50, true, true);
let mut keymap = Keymap::new();
keymap.clear.0 = KeyCode::Char('F');
setup
.terminal
.draw(|f| {
super::draw(
f,
AppColors::new(),
INSPECT_DATA.clone(),
&setup.gui_state,
&keymap,
);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
// Assert border colors
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) {
(0 | 49, _) | (_, 0 | 99) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::LightCyan);
}
_ => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::Gray);
}
}
}
}
}
#[test]
/// Test a inspect container with custom keymap for both clear keys
fn test_draw_blocks_inspect_custom_keymap_clear_two() {
let mut setup = test_setup(100, 50, true, true);
let mut keymap = Keymap::new();
keymap.clear.0 = KeyCode::Char('F');
keymap.clear.1 = Some(KeyCode::Char('Z'));
setup
.terminal
.draw(|f| {
super::draw(
f,
AppColors::new(),
INSPECT_DATA.clone(),
&setup.gui_state,
&keymap,
);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
// Assert border colors
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) {
(0 | 49, _) | (_, 0 | 99) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::LightCyan);
}
_ => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::Gray);
}
}
}
}
}
#[test]
/// Test a inspect container with custom keymap for one inspect key
fn test_draw_blocks_inspect_custom_keymap_inspect_one() {
let mut setup = test_setup(100, 50, true, true);
let mut keymap = Keymap::new();
keymap.inspect.0 = KeyCode::Char('4');
setup
.terminal
.draw(|f| {
super::draw(
f,
AppColors::new(),
INSPECT_DATA.clone(),
&setup.gui_state,
&keymap,
);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
// Assert border colors
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) {
(0 | 49, _) | (_, 0 | 99) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::LightCyan);
}
_ => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::Gray);
}
}
}
}
}
#[test]
/// Test a inspect container with custom keymap for both inspect keys
fn test_draw_blocks_inspect_custom_keymap_inspect_two() {
let mut setup = test_setup(100, 50, true, true);
let mut keymap = Keymap::new();
keymap.inspect.0 = KeyCode::Char('4');
keymap.inspect.1 = Some(KeyCode::Char('5'));
setup
.terminal
.draw(|f| {
super::draw(
f,
AppColors::new(),
INSPECT_DATA.clone(),
&setup.gui_state,
&keymap,
);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
// Assert border colors
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) {
(0 | 49, _) | (_, 0 | 99) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::LightCyan);
}
_ => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::Gray);
}
}
}
}
}
#[test]
/// Test a inspect container with all custom keymaps
fn test_draw_blocks_inspect_custom_keymap_all() {
let mut setup = test_setup(100, 50, true, true);
let mut keymap = Keymap::new();
keymap.clear.0 = KeyCode::Char('F');
keymap.clear.1 = Some(KeyCode::Char('Z'));
keymap.inspect.0 = KeyCode::Char('4');
keymap.inspect.1 = Some(KeyCode::Char('5'));
setup
.terminal
.draw(|f| {
super::draw(
f,
AppColors::new(),
INSPECT_DATA.clone(),
&setup.gui_state,
&keymap,
);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
// Assert border colors
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) {
(0 | 49, _) | (_, 0 | 99) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::LightCyan);
}
_ => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::Gray);
}
}
}
}
}
#[test]
/// Inspect details are offset 10 in x and y axis
fn test_draw_blocks_inspect_offset() {
let mut setup = test_setup(100, 50, true, true);
// Why does one need to draw first, although it *should* be impossible to scroll before an inital drawing
setup
.terminal
.draw(|f| {
super::draw(
f,
AppColors::new(),
INSPECT_DATA.clone(),
&setup.gui_state,
&Keymap::new(),
);
})
.unwrap();
{
let mut gui_state = setup.gui_state.lock();
for _ in 0..=9 {
gui_state.set_inspect_offset(&crate::app_data::ScrollDirection::Down);
gui_state.set_inspect_offset(&crate::app_data::ScrollDirection::Right);
}
}
setup
.terminal
.draw(|f| {
super::draw(
f,
AppColors::new(),
INSPECT_DATA.clone(),
&setup.gui_state,
&Keymap::new(),
);
})
.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) {
(0 | 49, _) | (_, 0 | 99) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::LightCyan);
}
_ => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::Gray);
}
}
}
}
}
#[test]
/// Inspect details are offset to the maximum allowed
fn test_draw_blocks_inspect_offset_max() {
let mut setup = test_setup(100, 50, true, true);
// Why does one need to draw first, although it *should* be impossible to scroll before an inital drawing
setup
.terminal
.draw(|f| {
super::draw(
f,
AppColors::new(),
INSPECT_DATA.clone(),
&setup.gui_state,
&Keymap::new(),
);
})
.unwrap();
// Lazy way of getting the max offset
{
let mut gui_state = setup.gui_state.lock();
for _ in 0..=1000 {
gui_state.set_inspect_offset(&crate::app_data::ScrollDirection::Down);
gui_state.set_inspect_offset(&crate::app_data::ScrollDirection::Right);
}
}
setup
.terminal
.draw(|f| {
super::draw(
f,
AppColors::new(),
INSPECT_DATA.clone(),
&setup.gui_state,
&Keymap::new(),
);
})
.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) {
(0 | 49, _) | (_, 0 | 99) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::LightCyan);
}
_ => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::Gray);
}
}
}
}
}
fn gen_container_inspect_response() -> ContainerInspectResponse {
ContainerInspectResponse {
id: Some("0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7".to_owned()),
created: Some("2026-01-23T22:20:19.927967311Z".to_owned()),
path: Some("docker-entrypoint.sh".to_owned()),
args: Some(vec!["postgres".to_owned()]),
state: Some(ContainerState {
status: Some(ContainerStateStatusEnum::RUNNING),
running: Some(true),
paused: Some(false),
restarting: Some(false),
oom_killed: Some(false),
dead: Some(false),
pid: Some(782),
exit_code: Some(0),
error: Some("".to_owned()),
started_at: Some("2026-01-30T08:09:01.574885915Z".to_owned()),
finished_at: Some("2026-01-30T08:09:01.180567927Z".to_owned()),
health: None,
}),
image: Some("sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352".to_owned()),
resolv_conf_path: Some("/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7/resolv.conf".to_owned()),
hostname_path: Some("/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7/hostname".to_owned()),
hosts_path: Some("/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7/hosts".to_owned()),
log_path: Some("/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7-json.log".to_owned()),
name: Some("/postgres".to_owned()),
restart_count: Some(0),
driver: Some("overlay2".to_owned()),
platform: Some("linux".to_owned()),
image_manifest_descriptor: None,
mount_label: Some("".to_owned()),
process_label: Some("".to_owned()),
app_armor_profile: Some("".to_owned()),
exec_ids: None,
host_config: Some(HostConfig {
cpu_shares: Some(0),
memory: Some(1073741824),
cgroup_parent: Some("".to_owned()),
blkio_weight: Some(0),
blkio_weight_device: None,
blkio_device_read_bps: None,
blkio_device_write_bps: None,
blkio_device_read_iops: None,
blkio_device_write_iops: None,
cpu_period: Some(0),
cpu_quota: Some(0),
cpu_realtime_period: Some(0),
cpu_realtime_runtime: Some(0),
cpuset_cpus: Some("".to_owned()),
cpuset_mems: Some("".to_owned()),
devices: None,
device_cgroup_rules: None,
device_requests: None,
memory_reservation: Some(0),
memory_swap: Some(2147483648),
memory_swappiness: None,
nano_cpus: Some(0),
oom_kill_disable: Some(false),
init: None,
pids_limit: None,
ulimits: None,
cpu_count: Some(0),
cpu_percent: Some(0),
io_maximum_iops: Some(0),
io_maximum_bandwidth: Some(0),
binds: None,
container_id_file: Some("".to_owned()),
log_config: Some(HostConfigLogConfig {
typ: Some("json-file".to_owned()),
config: Some(HashMap::new()),
}),
network_mode: Some("oxker-examaple-net".to_owned()),
port_bindings: Some(HashMap::new()),
restart_policy: Some(RestartPolicy {
name: Some(RestartPolicyNameEnum::ALWAYS),
maximum_retry_count: Some(0),
}),
auto_remove: Some(false),
volume_driver: Some("".to_owned()),
volumes_from: None,
mounts: None,
console_size: Some(vec![0, 0]),
annotations: None,
cap_add: None,
cap_drop: None,
cgroupns_mode: Some(bollard::secret::HostConfigCgroupnsModeEnum::HOST),
dns: Some(vec![]),
dns_options: Some(vec![]),
dns_search: Some(vec![]),
extra_hosts: Some(vec![]),
group_add: None,
ipc_mode: Some("private".to_owned()),
cgroup: Some("".to_owned()),
links: None,
oom_score_adj: Some(0),
pid_mode: Some("".to_owned()),
privileged: Some(false),
publish_all_ports: Some(false),
readonly_rootfs: Some(false),
security_opt: None,
storage_opt: None,
tmpfs: None,
uts_mode: Some("".to_owned()),
userns_mode: Some("".to_owned()),
shm_size: Some(268435456),
sysctls: None,
runtime: Some("runc".to_owned()),
isolation: Some(bollard::secret::HostConfigIsolationEnum::EMPTY),
masked_paths: Some(vec![
"/proc/acpi".to_owned(),
"/proc/asound".to_owned(),
"/proc/interrupts".to_owned(),
"/proc/kcore".to_owned(),
"/proc/keys".to_owned(),
"/proc/latency_stats".to_owned(),
"/proc/sched_debug".to_owned(),
"/proc/scsi".to_owned(),
"/proc/timer_list".to_owned(),
"/proc/timer_stats".to_owned(),
"/sys/devices/virtual/powercap".to_owned(),
"/sys/firmware".to_owned(),
]),
readonly_paths: Some(vec![
"/proc/bus".to_owned(),
"/proc/fs".to_owned(),
"/proc/irq".to_owned(),
"/proc/sys".to_owned(),
"/proc/sysrq-trigger".to_owned(),
]),
}),
graph_driver: Some(DriverData {
name: "overlay2".to_owned(),
data: HashMap::from([
("LowerDir".to_owned(), "/var/lib/docker/overlay2/b8dae7c82251b8dadc084dbcaceec47b3d48a5ba9d055a59934a8b88d18569ea-init/diff:/var/lib/docker/overlay2/51b93846f7ba3e00cb1ed86564e3e1d7c30df2bb1cd5a8469d54625f1e5a2eca/diff:/var/lib/docker/overlay2/c1364ead843d3af87ce286013b6301329d3089422b22b001e156e45d29b5b4dd/diff:/var/lib/docker/overlay2/0e6dc322cad77b1db3906a3a4e5e6d6b80fbffd138437e550d8849fcf4f4c1f2/diff:/var/lib/docker/overlay2/cc0f967a7471cf06e0c9ad3d474650c668a4cf0c02efe20e9c250c436f93033b/diff:/var/lib/docker/overlay2/5c59e0919969987c96a5d0e0a512a0a1a0f67ea747596af9a9c14a9566198d91/diff:/var/lib/docker/overlay2/d7709b7685c9704e1e392c515b6155517270541f6ccde426ef784403e1681fca/diff:/var/lib/docker/overlay2/c891528563fff91bffaf07416e77bcd3bdebb03e5d32ed0e3d4ee1ec5e80e880/diff:/var/lib/docker/overlay2/2b25c179a432c35cc599a082cd709c8c9a1523f8d1959f72fda21fc76e50ad00/diff:/var/lib/docker/overlay2/3b409d2f7a2455578148892302823a7f03c7c36482d08bb68fd6c1aeeec05f05/diff:/var/lib/docker/overlay2/55dbb2fab0ae8bb3bfe8183093cdd576686f7333e2b2c41e6e4178a7b6407554/diff".to_owned()),
("MergedDir".to_owned(), "/var/lib/docker/overlay2/b8dae7c82251b8dadc084dbcaceec47b3d48a5ba9d055a59934a8b88d18569ea/merged".to_owned()),
("WorkDir".to_owned(), "/var/lib/docker/overlay2/b8dae7c82251b8dadc084dbcaceec47b3d48a5ba9d055a59934a8b88d18569ea/work".to_owned()),
("ID".to_owned(), "0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7".to_owned()),
("UpperDir".to_owned(), "/var/lib/docker/overlay2/b8dae7c82251b8dadc084dbcaceec47b3d48a5ba9d055a59934a8b88d18569ea/diff".to_owned()),
]),
}),
storage: None,
size_rw: None,
size_root_fs: None,
mounts: Some(vec![MountPoint {
typ: Some(MountPointTypeEnum::VOLUME),
name: Some("93bc4e4c8d3823964b58105a99a7b3a7e02c801d5560338bdaf7589966a1b02d".to_owned()),
source: Some("/var/lib/docker/volumes/93bc4e4c8d3823964b58105a99a7b3a7e02c801d5560338bdaf7589966a1b02d/_data".to_owned()),
destination: Some("/var/lib/postgresql/data".to_owned()),
driver: Some("local".to_owned()),
mode: Some("".to_owned()),
rw: Some(true),
propagation: Some("".to_owned()),
}]),
config: Some(ContainerConfig {
hostname: Some("0bdea64212f9".to_owned()),
domainname: Some("".to_owned()),
user: Some("".to_owned()),
attach_stdin: Some(false),
attach_stdout: Some(true),
attach_stderr: Some(true),
exposed_ports: Some(vec!["5432/tcp".to_owned()]),
tty: Some(false),
open_stdin: Some(false),
stdin_once: Some(false),
env: Some(vec![
"POSTGRES_PASSWORD=never_use_this_password_in_production".to_owned(),
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_owned(),
"GOSU_VERSION=1.19".to_owned(),
"LANG=en_US.utf8".to_owned(),
"PG_MAJOR=17".to_owned(),
"PG_VERSION=17.7".to_owned(),
"PG_SHA256=ef9e343302eccd33112f1b2f0247be493cb5768313adeb558b02de8797a2e9b5".to_owned(),
"DOCKER_PG_LLVM_DEPS=llvm19-dev \t\tclang19".to_owned(),
"PGDATA=/var/lib/postgresql/data".to_owned(),
]),
cmd: Some(vec!["postgres".to_owned()]),
healthcheck: None,
args_escaped: None,
image: Some("postgres:17-alpine".to_owned()),
volumes: Some(vec!["/var/lib/postgresql/data".to_owned()]),
working_dir: Some("/".to_owned()),
entrypoint: Some(vec!["docker-entrypoint.sh".to_owned()]),
network_disabled: None,
on_build: None,
labels: Some(HashMap::from([
("com.docker.compose.oneoff".to_owned(), "False".to_owned()),
("com.docker.compose.project.config_files".to_owned(), "/workspaces/oxker/docker/docker-compose.yml".to_owned()),
("com.docker.compose.image".to_owned(), "sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352".to_owned()),
("com.docker.compose.project.working_dir".to_owned(), "/workspaces/oxker/docker".to_owned()),
("com.docker.compose.service".to_owned(), "postgres".to_owned()),
("com.docker.compose.config-hash".to_owned(), "e06d69ffb3f9b69dd51b356b60c2297df57caf0da16792ccafaabffdb920e443".to_owned()),
("com.docker.compose.depends_on".to_owned(), "".to_owned()),
("com.docker.compose.container-number".to_owned(), "1".to_owned()),
("com.docker.compose.version".to_owned(), "2.40.3".to_owned()),
("com.docker.compose.project".to_owned(), "docker".to_owned()),
])),
stop_signal: Some("SIGINT".to_owned()),
stop_timeout: None,
shell: None,
}),
network_settings: Some(NetworkSettings {
sandbox_id: Some("dab64a66594dd8d06478184e2928c81acdcd9c931f643bd5ca62b7edb6345f8d".to_owned()),
sandbox_key: Some("/var/run/docker/netns/dab64a66594d".to_owned()),
ports: Some(HashMap::from([("5432/tcp".to_owned(), None)])),
networks: Some(HashMap::from([(
"oxker-examaple-net".to_owned(),
EndpointSettings {
ipam_config: None,
links: None,
mac_address: Some("a2:bd:4e:61:25:c7".to_owned()),
aliases: Some(vec!["postgres".to_owned(), "postgres".to_owned()]),
driver_opts: None,
gw_priority: Some(0),
network_id: Some("3cbeb475d81676f89a7aa205d8749ec2ad78d685e45d77b638992956f6dc569a".to_owned()),
endpoint_id: Some("31718069b2a3ea77487f3ece36b014d5d1329bc3294568e2621e5c0999071bed".to_owned()),
gateway: Some("172.19.0.1".to_owned()),
ip_address: Some("172.19.0.4".to_owned()),
ip_prefix_len: Some(16),
ipv6_gateway: Some("".to_owned()),
global_ipv6_address: Some("".to_owned()),
global_ipv6_prefix_len: Some(0),
dns_names: Some(vec!["postgres".to_owned(), "0bdea64212f9".to_owned()]),
},
)])),
}),
}
}
}
+4 -4
View File
@@ -14,7 +14,7 @@ use crate::{
ui::{FrameData, GuiState, SelectablePanel, Status},
};
use super::{RIGHT_ARROW, generate_block};
use super::{SELECT_ARROW, generate_block};
/// Draw the logs panel
pub fn draw(
@@ -52,7 +52,7 @@ pub fn draw(
} else if fd.color_logs {
let items = List::new(logs)
.block(block)
.highlight_symbol(RIGHT_ARROW)
.highlight_symbol(SELECT_ARROW)
.scroll_padding(padding)
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
// This should always return Some, as logs is not empty
@@ -63,7 +63,7 @@ pub fn draw(
let items = List::new(logs)
.fg(colors.logs.text)
.block(block)
.highlight_symbol(RIGHT_ARROW)
.highlight_symbol(SELECT_ARROW)
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
// This should always return Some, as logs is not empty
if let Some(log_state) = app_data.lock().get_log_state() {
@@ -309,7 +309,7 @@ mod tests {
);
})
.unwrap();
setup.app_data.lock().log_scroll(&ScrollDirection::Previous);
setup.app_data.lock().log_scroll(&ScrollDirection::Up);
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
+14 -10
View File
@@ -20,26 +20,30 @@ pub mod filter;
pub mod headers;
pub mod help;
pub mod info;
pub mod inspect;
pub mod logs;
pub mod popup;
pub mod ports;
pub mod search_logs;
pub const NAME_TEXT: &str = r#"
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 "#;
pub const NAME_TEXT: &str = r#" 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 "#;
pub const NAME: &str = env!("CARGO_PKG_NAME");
pub const REPO: &str = env!("CARGO_PKG_REPOSITORY");
pub const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
pub const MARGIN: &str = " ";
pub const RIGHT_ARROW: &str = "";
pub const SELECT_ARROW: &str = "";
// TODO use me all over the place
pub const LEFT_ARROW: &str = "";
pub const RIGHT_ARROW: &str = "";
pub const DOWN_ARROW: &str = "";
pub const UP_ARROW: &str = "";
pub const CIRCLE: &str = "";
#[cfg(not(test))]
+5 -5
View File
@@ -241,11 +241,11 @@ mod tests {
setup
.app_data
.lock()
.log_scroll(&crate::app_data::ScrollDirection::Previous);
.log_scroll(&crate::app_data::ScrollDirection::Up);
setup
.app_data
.lock()
.log_scroll(&crate::app_data::ScrollDirection::Previous);
.log_scroll(&crate::app_data::ScrollDirection::Up);
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
@@ -298,7 +298,7 @@ mod tests {
setup
.app_data
.lock()
.log_scroll(&crate::app_data::ScrollDirection::Previous);
.log_scroll(&crate::app_data::ScrollDirection::Up);
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
@@ -404,7 +404,7 @@ mod tests {
setup
.app_data
.lock()
.log_scroll(&crate::app_data::ScrollDirection::Previous);
.log_scroll(&crate::app_data::ScrollDirection::Up);
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
@@ -433,7 +433,7 @@ mod tests {
setup
.app_data
.lock()
.log_scroll(&crate::app_data::ScrollDirection::Previous);
.log_scroll(&crate::app_data::ScrollDirection::Up);
let mut colors = AppColors::new();
@@ -2,42 +2,28 @@
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 │ "
" │ │ "
" │ ( tab ) or ( shift+tab ) change panels │ "
" │ ( ↑ ↓ ) or ( j k ) or ( Home End ) scroll vertically │ "
" │ ( ← → ) horizontal scroll across logs │ "
" │ ( ctrl ) increase scroll speed, used in conjunction scroll keys │ "
" │ ( 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 │ "
" │ ( F1 ) or ( / ) enter filter mode "
" │ ( # ) enter log search mode │ "
" │ ( 0 ) stop sort │ "
" │ ( 1 - 9 ) sort by header - or click header │ "
" │ ( - = ) change log section height │ "
" │ ( \ ) toggle log section visibility │ "
" │ ( esc ) close dialog │ "
" │ ( q ) 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 │ "
" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba │ "
" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y config location: /home/user/.config/oxker/config.toml │ "
" │ 8b d8 )888( 8888( 8PP""""""" 88 export location: /test_dir │ "
" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 logs timezone: Etc/UTC │ "
" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "
" │ a work in progress, all and any input appreciated │ "
" │ A simple tui to view & control docker containers https://github.com/mrjackwills/oxker │ "
" │ │ "
" │ Keymap │ "
" │ q quit c Esc close dialog │ "
" │ Down Up j k Home End scroll vertically Left Right scroll horizontally │ "
" │ Control increase scroll speed Enter send docker command │ "
" │ e exec into a container i container inspect mode │ "
" │ / F1 filter mode # log search mode │ "
" │ h toggle this panel f force clear screen and redraw │ "
" │ - = change log section height \ toggle of section visibility │ "
" │ 1 ~ 9 sort by header - or click header 0 stop sort │ "
" │ Tab Back Tab change panel m toggle mouse capture - allows text selection │ "
" │ s save logs to file │ "
" ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ "
" "
@@ -0,0 +1,29 @@
---
source: src/ui/draw_blocks/help.rs
expression: setup.terminal.backend()
---
" "
" ╭ 0.00.000 ────────────────────────────────────────────────────────────────────────────────────────────────────────╮ "
" │ 88 │ "
" │ 88 │ "
" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba │ "
" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y config location: /home/user/.config/oxker/config.toml │ "
" │ 8b d8 )888( 8888( 8PP""""""" 88 export location: /test_dir │ "
" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 logs timezone: Etc/UTC │ "
" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "
" │ a work in progress, all and any input appreciated │ "
" │ A simple tui to view & control docker containers https://github.com/mrjackwills/oxker │ "
" │ │ "
" │ Keymap │ "
" │ q quit c Esc close dialog │ "
" │ Down Up j k Home End scroll vertically Left Right scroll horizontally │ "
" │ Control increase scroll speed Enter send docker command │ "
" │ e exec into a container i container inspect mode │ "
" │ / F1 filter mode # log search mode │ "
" │ h toggle this panel f force clear screen and redraw │ "
" │ - = change log section height \ toggle of section visibility │ "
" │ 1 ~ 9 sort by header - or click header 0 stop sort │ "
" │ Tab Back Tab change panel m toggle mouse capture - allows text selection │ "
" │ s save logs to file │ "
" ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ "
" "
@@ -3,41 +3,41 @@ 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 "
" "
" ( tab ) or ( shift+tab ) change panels "
" ( ↑ ↓ ) or ( j k ) or ( Home End ) scroll vertically "
" ( ← → ) horizontal scroll across logs "
" ( ctrl ) increase scroll speed, used in conjunction scroll keys "
" │ ( 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 │ "
" ( F1 ) or ( / ) enter filter mode "
" ( # ) enter log search mode "
" ( 0 ) stop sort "
" ( 1 - 9 ) sort by header - or click header "
" ( - = ) change log section height "
" │ ( \ ) toggle log section visibility "
" │ ( esc ) close dialog │ "
" │ ( q ) 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 │"
"│ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPY │"
"│ a8" "8a `Y8, ,8P' 88 ,a8" a8P____ │"
"│ 8b d8 )888( 8888( 8PP"""" │"
"│ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, │"
"│ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd │"
"│ a work in progress, all and any input app │"
"│ A simple tui to view & control docker cont https://github.com/mrjackwills/oxker │"
" │"
" Keymap │"
"│ q quit c Esc close dialog │"
"│ Down Up j k Home End scrol Left Right scroll horizontally │"
"│ Control incre Enter send docker command │"
"│ e exec i container inspect mode │"
"│ / F1 filte # log search mode │"
"│ h toggl f force clear screen and redraw │"
"│ - = chang \ toggle of section visibility │"
"│ 1 ~ 9 sort 0 stop sort │"
"│ Tab Back Tab chang m toggle mouse capture - allows text selection │"
"│ s save │"
"╰─────────────────────────────────────────────────────────────────────────────────────╯"
" "
" "
" "
" "
" "
" "
" "
" "
@@ -2,53 +2,28 @@
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 "
" "
" │ ( t ) select next panel "
" │ ( u ) select previous panel "
" │ ( o ) scroll list down by one "
" │ ( s ) scroll list up by one "
" │ ( p ) scroll list to end "
" │ ( q ) scroll list to start "
" │ ( h ) horizontal scroll logs right "
" │ ( g ) horizontal scroll logs left "
" │ ( Alt ) increase scroll speed, used in conjunction scroll keys "
" │ ( 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 ) save logs to file │ "
" │ ( 6 ) toggle mouse capture - if disabled, text on screen can be selected & copied │ "
" │ ( e ) enter filter mode │ "
" │ ( 7 ) enter log search mode │ "
" │ ( 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 │ "
" │ ( 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 "
" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba │ "
" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y config location: /home/user/.config/oxker/config.toml │ "
" │ 8b d8 )888( 8888( 8PP""""""" 88 export location: /test_dir │ "
" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 logs timezone: Etc/UTC │ "
" │ "
" │ Keymap "
" │ n quit a close dialog "
" │ p s scroll vertically i j scroll horizontally │ "
" │ r scroll to start q scroll to end "
" │ Alt increase scroll speed Enter send docker command │ "
" d exec into a container e container inspect mode │ "
" f filter mode g log search mode │ "
" 5 toggle this panel h force clear screen and redraw │ "
" │ k l change log section height m toggle of section visibility │ "
" z sort by name 1 sort by state │ "
" 2 sort by status v sort by CPU │ "
" y sort by memory w sort by ID │ "
" x sort by Image 0 sort by RX │ "
" │ 3 sort by TX 4 stop sort │ "
" t u change panel 6 toggle mouse capture - allows text selection │ "
" o save logs to file │ "
" ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ "
@@ -0,0 +1,34 @@
---
source: src/ui/draw_blocks/help.rs
expression: setup.terminal.backend()
---
" "
" ╭ 0.00.000 ────────────────────────────────────────────────────────────────────────────────────────────────────────╮ "
" │ 88 │ "
" │ 88 │ "
" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba │ "
" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y config location: /home/user/.config/oxker/config.toml │ "
" │ 8b d8 )888( 8888( 8PP""""""" 88 export location: /test_dir │ "
" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 logs timezone: Etc/UTC │ "
" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "
" │ a work in progress, all and any input appreciated │ "
" │ A simple tui to view & control docker containers https://github.com/mrjackwills/oxker │ "
" │ │ "
" │ Keymap │ "
" │ 0 quit a b close dialog │ "
" │ 4 Caps Lock Scroll Lock scroll vertically q s r scroll horizontally │ "
" │ 8 scroll to start 6 7 scroll to end │ "
" │ Alt increase scroll speed Enter send docker command │ "
" │ g exec into a container i j container inspect mode │ "
" │ k filter mode m n log search mode │ "
" │ F5 F6 toggle this panel o force clear screen and redraw │ "
" │ u w v change log section height y z toggle of section visibility │ "
" │ Begin Menu sort by name Page Up Pause sort by state │ "
" │ Print Screen sort by status Down sort by CPU │ "
" │ Home sort by memory Back Tab sort by ID │ "
" │ End Esc sort by Image Num Lock sort by RX │ "
" │ F1 F2 sort by TX F3 stop sort │ "
" │ Print Screen Left Up change panel F7 toggle mouse capture - allows text selection │ "
" │ 2 3 save logs to file │ "
" ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ "
" "
@@ -0,0 +1,34 @@
---
source: src/ui/draw_blocks/help.rs
expression: setup.terminal.backend()
---
" "
" ╭ 0.00.000 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ "
" │ 88 │ "
" │ 88 │ "
" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ "
" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 config location: /home/user/.config/oxker/config.toml │ "
" │ 8b d8 )888( 8888( 8PP""""""" 88 export location: /test_dir │ "
" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 logs timezone: Etc/UTC │ "
" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "
" │ a work in progress, all and any input appreciated │ "
" │ A simple tui to view & control docker containers https://github.com/mrjackwills/oxker │ "
" │ │ "
" │ Keymap │ "
" │ 0 1 quit a b close dialog │ "
" │ 4 Caps Lock 5 Scroll Lock scroll vertically q s r t scroll horizontally │ "
" │ 8 9 scroll to start 6 7 scroll to end │ "
" │ Alt increase scroll speed Enter send docker command │ "
" │ g h exec into a container i j container inspect mode │ "
" │ k l filter mode m n log search mode │ "
" │ F5 F6 toggle this panel o p force clear screen and redraw │ "
" │ u w v x change log section height y z toggle of section visibility │ "
" │ Begin Menu sort by name Page Up Pause sort by state │ "
" │ Print Screen Tab sort by status Down Del sort by CPU │ "
" │ Home Insert sort by memory Back Tab Backspace sort by ID │ "
" │ End Esc sort by Image Num Lock Page Down sort by RX │ "
" │ F1 F2 sort by TX F3 F4 stop sort │ "
" │ Print Screen Left Up Right change panel F7 F8 toggle mouse capture - allows text selection │ "
" │ 2 3 save logs to file │ "
" ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ "
" "
@@ -1,54 +0,0 @@
---
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 ) or ( T ) select previous panel │ "
" │ ( n ) or ( N ) scroll list down by one │ "
" │ ( r ) or ( R ) scroll list up by one │ "
" │ ( 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 │ "
" │ ( Alt ) increase scroll speed, used in conjunction scroll keys │ "
" │ ( 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 │ "
" │ ( e ) or ( E ) enter filter mode │ "
" │ ( m ) or ( M ) enter log search 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 │ "
" │ │ "
" ╰────────────────────────────────────────────────────────────────────────────────────────────────────╯ "
@@ -0,0 +1,29 @@
---
source: src/ui/draw_blocks/help.rs
expression: setup.terminal.backend()
---
" "
" ╭ 0.00.000 ──────────────────────────────────────────────────────────────────────────────────────────────────────╮ "
" │ 88 │ "
" │ 88 │ "
" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYb │ "
" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' " │ "
" │ 8b d8 )888( 8888( 8PP""""""" 88 export location: /test_dir │ "
" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 logs timezone: Etc/UTC │ "
" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "
" │ a work in progress, all and any input appreciated │ "
" │ A simple tui to view & control docker containers https://github.com/mrjackwills/oxker │ "
" │ │ "
" │ Keymap │ "
" │ q quit c Esc close dialog │ "
" │ Down Up j k Home End scroll vertically Left Right scroll horizontally │ "
" │ Control increase scroll speed Enter send docker command │ "
" │ e exec into a container i container inspect mode │ "
" │ / F1 filter mode # log search mode │ "
" │ h toggle this panel f force clear screen and redraw │ "
" │ - = change log section height \ toggle of section visibility │ "
" │ 1 ~ 9 sort by header - or click header 0 stop sort │ "
" │ Tab Back Tab change panel m toggle mouse capture - allows text selection │ "
" │ s save logs to file │ "
" ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ "
" "
@@ -0,0 +1,29 @@
---
source: src/ui/draw_blocks/help.rs
expression: setup.terminal.backend()
---
" "
" ╭ 0.00.000 ────────────────────────────────────────────────────────────────────────────────────────────────────────╮ "
" │ 88 │ "
" │ 88 │ "
" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba │ "
" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y │ "
" │ 8b d8 )888( 8888( 8PP""""""" 88 config location: /home/user/.config/oxker/config.toml │ "
" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 logs timezone: Etc/UTC │ "
" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "
" │ a work in progress, all and any input appreciated │ "
" │ A simple tui to view & control docker containers https://github.com/mrjackwills/oxker │ "
" │ │ "
" │ Keymap │ "
" │ q quit c Esc close dialog │ "
" │ Down Up j k Home End scroll vertically Left Right scroll horizontally │ "
" │ Control increase scroll speed Enter send docker command │ "
" │ e exec into a container i container inspect mode │ "
" │ / F1 filter mode # log search mode │ "
" │ h toggle this panel f force clear screen and redraw │ "
" │ - = change log section height \ toggle of section visibility │ "
" │ 1 ~ 9 sort by header - or click header 0 stop sort │ "
" │ Tab Back Tab change panel m toggle mouse capture - allows text selection │ "
" │ s save logs to file │ "
" ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ "
" "
@@ -0,0 +1,29 @@
---
source: src/ui/draw_blocks/help.rs
expression: setup.terminal.backend()
---
" "
" ╭ 0.00.000 ────────────────────────────────────────────────────────────────────────────────────────────────────────╮ "
" │ 88 │ "
" │ 88 │ "
" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba │ "
" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y │ "
" │ 8b d8 )888( 8888( 8PP""""""" 88 config location: /home/user/.config/oxker/config.toml │ "
" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 export location: /test_dir │ "
" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "
" │ a work in progress, all and any input appreciated │ "
" │ A simple tui to view & control docker containers https://github.com/mrjackwills/oxker │ "
" │ │ "
" │ Keymap │ "
" │ q quit c Esc close dialog │ "
" │ Down Up j k Home End scroll vertically Left Right scroll horizontally │ "
" │ Control increase scroll speed Enter send docker command │ "
" │ e exec into a container i container inspect mode │ "
" │ / F1 filter mode # log search mode │ "
" │ h toggle this panel f force clear screen and redraw │ "
" │ - = change log section height \ toggle of section visibility │ "
" │ 1 ~ 9 sort by header - or click header 0 stop sort │ "
" │ Tab Back Tab change panel m toggle mouse capture - allows text selection │ "
" │ s save logs to file │ "
" ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ "
" "
@@ -1,54 +0,0 @@
---
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 │ "
" │ │ "
" │ ( 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 │ "
" │ ( p ) scroll list to end │ "
" │ ( 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 conjunction scroll keys │ "
" │ ( 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 ( # ) toggle mouse capture - if disabled, text on screen can be selected & copied │ "
" │ ( e ) or ( E ) enter filter mode │ "
" │ ( 8 ) enter log search 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 │ "
" │ │ "
" ╰────────────────────────────────────────────────────────────────────────────────────────────╯ "
@@ -1,43 +0,0 @@
---
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 │ "
" │ │ "
" │ logs timezone: Asia/Tokyo │ "
" │ │ "
" │ ( tab ) or ( shift+tab ) change panels │ "
" │ ( ↑ ↓ ) or ( j k ) or ( Home End ) scroll vertically │ "
" │ ( ← → ) horizontal scroll across logs │ "
" │ ( ctrl ) increase scroll speed, used in conjunction scroll keys │ "
" │ ( 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 │ "
" │ ( F1 ) or ( / ) enter filter mode │ "
" │ ( # ) enter log search mode │ "
" │ ( 0 ) stop sort │ "
" │ ( 1 - 9 ) sort by header - or click header │ "
" │ ( - = ) change log section height │ "
" │ ( \ ) toggle log section visibility │ "
" │ ( esc ) close dialog │ "
" │ ( q ) quit at any time │ "
" │ │ "
" │ currently an early work in progress, all and any input appreciated │ "
" │ https://github.com/mrjackwills/oxker │ "
" │ │ "
" │ │ "
" ╰───────────────────────────────────────────────────────────────────────────────────╯ "
@@ -0,0 +1,54 @@
---
source: src/ui/draw_blocks/inspect.rs
expression: setup.terminal.backend()
---
"╭───────────────────── inspecting: postgres 0bdea642 - c or Esc or i to exit ─────────────────────╮"
"│ "Id": "0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7", │"
"│ "Created": "2026-01-23T22:20:19.927967311Z", │"
"│ "Path": "docker-entrypoint.sh", │"
"│ "Args": [ │"
"│ "postgres" │"
"│ ], │"
"│ "State": { │"
"│ "Status": "running", │"
"│ "Running": true, │"
"│ "Paused": false, │"
"│ "Restarting": false, │"
"│ "OOMKilled": false, │"
"│ "Dead": false, │"
"│ "Pid": 782, │"
"│ "ExitCode": 0, │"
"│ "Error": "", │"
"│ "StartedAt": "2026-01-30T08:09:01.574885915Z", │"
"│ "FinishedAt": "2026-01-30T08:09:01.180567927Z" │"
"│ }, │"
"│ "Image": "sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352", │"
"│ "ResolvConfPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c853│"
"│ "HostnamePath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358│"
"│ "HostsPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456│"
"│ "LogPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc│"
"│ "Name": "/postgres", │"
"│ "RestartCount": 0, │"
"│ "Driver": "overlay2", │"
"│ "Platform": "linux", │"
"│ "MountLabel": "", │"
"│ "ProcessLabel": "", │"
"│ "AppArmorProfile": "", │"
"│ "HostConfig": { │"
"│ "CpuShares": 0, │"
"│ "Memory": 1073741824, │"
"│ "CgroupParent": "", │"
"│ "BlkioWeight": 0, │"
"│ "CpuPeriod": 0, │"
"│ "CpuQuota": 0, │"
"│ "CpuRealtimePeriod": 0, │"
"│ "CpuRealtimeRuntime": 0, │"
"│ "CpusetCpus": "", │"
"│ "CpusetMems": "", │"
"│ "MemoryReservation": 0, │"
"│ "MemorySwap": 2147483648, │"
"│ "NanoCpus": 0, │"
"│ "OomKillDisable": false, │"
"│ "CpuCount": 0, │"
"│ "CpuPercent": 0, │"
"╰──────────────────────────────────── 0/158 ↓ 0/972 → ────────────────────────────────────╯"
@@ -0,0 +1,54 @@
---
source: src/ui/draw_blocks/inspect.rs
expression: setup.terminal.backend()
---
"╭─────────────────── inspecting: postgres 0bdea642 - F or Z or 4 or 5 to exit ────────────────────╮"
"│ "Id": "0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7", │"
"│ "Created": "2026-01-23T22:20:19.927967311Z", │"
"│ "Path": "docker-entrypoint.sh", │"
"│ "Args": [ │"
"│ "postgres" │"
"│ ], │"
"│ "State": { │"
"│ "Status": "running", │"
"│ "Running": true, │"
"│ "Paused": false, │"
"│ "Restarting": false, │"
"│ "OOMKilled": false, │"
"│ "Dead": false, │"
"│ "Pid": 782, │"
"│ "ExitCode": 0, │"
"│ "Error": "", │"
"│ "StartedAt": "2026-01-30T08:09:01.574885915Z", │"
"│ "FinishedAt": "2026-01-30T08:09:01.180567927Z" │"
"│ }, │"
"│ "Image": "sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352", │"
"│ "ResolvConfPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c853│"
"│ "HostnamePath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358│"
"│ "HostsPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456│"
"│ "LogPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc│"
"│ "Name": "/postgres", │"
"│ "RestartCount": 0, │"
"│ "Driver": "overlay2", │"
"│ "Platform": "linux", │"
"│ "MountLabel": "", │"
"│ "ProcessLabel": "", │"
"│ "AppArmorProfile": "", │"
"│ "HostConfig": { │"
"│ "CpuShares": 0, │"
"│ "Memory": 1073741824, │"
"│ "CgroupParent": "", │"
"│ "BlkioWeight": 0, │"
"│ "CpuPeriod": 0, │"
"│ "CpuQuota": 0, │"
"│ "CpuRealtimePeriod": 0, │"
"│ "CpuRealtimeRuntime": 0, │"
"│ "CpusetCpus": "", │"
"│ "CpusetMems": "", │"
"│ "MemoryReservation": 0, │"
"│ "MemorySwap": 2147483648, │"
"│ "NanoCpus": 0, │"
"│ "OomKillDisable": false, │"
"│ "CpuCount": 0, │"
"│ "CpuPercent": 0, │"
"╰──────────────────────────────────── 0/158 ↓ 0/972 → ────────────────────────────────────╯"
@@ -0,0 +1,54 @@
---
source: src/ui/draw_blocks/inspect.rs
expression: setup.terminal.backend()
---
"╭───────────────────── inspecting: postgres 0bdea642 - F or Esc or i to exit ─────────────────────╮"
"│ "Id": "0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7", │"
"│ "Created": "2026-01-23T22:20:19.927967311Z", │"
"│ "Path": "docker-entrypoint.sh", │"
"│ "Args": [ │"
"│ "postgres" │"
"│ ], │"
"│ "State": { │"
"│ "Status": "running", │"
"│ "Running": true, │"
"│ "Paused": false, │"
"│ "Restarting": false, │"
"│ "OOMKilled": false, │"
"│ "Dead": false, │"
"│ "Pid": 782, │"
"│ "ExitCode": 0, │"
"│ "Error": "", │"
"│ "StartedAt": "2026-01-30T08:09:01.574885915Z", │"
"│ "FinishedAt": "2026-01-30T08:09:01.180567927Z" │"
"│ }, │"
"│ "Image": "sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352", │"
"│ "ResolvConfPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c853│"
"│ "HostnamePath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358│"
"│ "HostsPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456│"
"│ "LogPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc│"
"│ "Name": "/postgres", │"
"│ "RestartCount": 0, │"
"│ "Driver": "overlay2", │"
"│ "Platform": "linux", │"
"│ "MountLabel": "", │"
"│ "ProcessLabel": "", │"
"│ "AppArmorProfile": "", │"
"│ "HostConfig": { │"
"│ "CpuShares": 0, │"
"│ "Memory": 1073741824, │"
"│ "CgroupParent": "", │"
"│ "BlkioWeight": 0, │"
"│ "CpuPeriod": 0, │"
"│ "CpuQuota": 0, │"
"│ "CpuRealtimePeriod": 0, │"
"│ "CpuRealtimeRuntime": 0, │"
"│ "CpusetCpus": "", │"
"│ "CpusetMems": "", │"
"│ "MemoryReservation": 0, │"
"│ "MemorySwap": 2147483648, │"
"│ "NanoCpus": 0, │"
"│ "OomKillDisable": false, │"
"│ "CpuCount": 0, │"
"│ "CpuPercent": 0, │"
"╰──────────────────────────────────── 0/158 ↓ 0/972 → ────────────────────────────────────╯"
@@ -0,0 +1,54 @@
---
source: src/ui/draw_blocks/inspect.rs
expression: setup.terminal.backend()
---
"╭────────────────────── inspecting: postgres 0bdea642 - F or Z or i to exit ──────────────────────╮"
"│ "Id": "0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7", │"
"│ "Created": "2026-01-23T22:20:19.927967311Z", │"
"│ "Path": "docker-entrypoint.sh", │"
"│ "Args": [ │"
"│ "postgres" │"
"│ ], │"
"│ "State": { │"
"│ "Status": "running", │"
"│ "Running": true, │"
"│ "Paused": false, │"
"│ "Restarting": false, │"
"│ "OOMKilled": false, │"
"│ "Dead": false, │"
"│ "Pid": 782, │"
"│ "ExitCode": 0, │"
"│ "Error": "", │"
"│ "StartedAt": "2026-01-30T08:09:01.574885915Z", │"
"│ "FinishedAt": "2026-01-30T08:09:01.180567927Z" │"
"│ }, │"
"│ "Image": "sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352", │"
"│ "ResolvConfPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c853│"
"│ "HostnamePath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358│"
"│ "HostsPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456│"
"│ "LogPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc│"
"│ "Name": "/postgres", │"
"│ "RestartCount": 0, │"
"│ "Driver": "overlay2", │"
"│ "Platform": "linux", │"
"│ "MountLabel": "", │"
"│ "ProcessLabel": "", │"
"│ "AppArmorProfile": "", │"
"│ "HostConfig": { │"
"│ "CpuShares": 0, │"
"│ "Memory": 1073741824, │"
"│ "CgroupParent": "", │"
"│ "BlkioWeight": 0, │"
"│ "CpuPeriod": 0, │"
"│ "CpuQuota": 0, │"
"│ "CpuRealtimePeriod": 0, │"
"│ "CpuRealtimeRuntime": 0, │"
"│ "CpusetCpus": "", │"
"│ "CpusetMems": "", │"
"│ "MemoryReservation": 0, │"
"│ "MemorySwap": 2147483648, │"
"│ "NanoCpus": 0, │"
"│ "OomKillDisable": false, │"
"│ "CpuCount": 0, │"
"│ "CpuPercent": 0, │"
"╰──────────────────────────────────── 0/158 ↓ 0/972 → ────────────────────────────────────╯"
@@ -0,0 +1,54 @@
---
source: src/ui/draw_blocks/inspect.rs
expression: setup.terminal.backend()
---
"╭───────────────────── inspecting: postgres 0bdea642 - c or Esc or 4 to exit ─────────────────────╮"
"│ "Id": "0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7", │"
"│ "Created": "2026-01-23T22:20:19.927967311Z", │"
"│ "Path": "docker-entrypoint.sh", │"
"│ "Args": [ │"
"│ "postgres" │"
"│ ], │"
"│ "State": { │"
"│ "Status": "running", │"
"│ "Running": true, │"
"│ "Paused": false, │"
"│ "Restarting": false, │"
"│ "OOMKilled": false, │"
"│ "Dead": false, │"
"│ "Pid": 782, │"
"│ "ExitCode": 0, │"
"│ "Error": "", │"
"│ "StartedAt": "2026-01-30T08:09:01.574885915Z", │"
"│ "FinishedAt": "2026-01-30T08:09:01.180567927Z" │"
"│ }, │"
"│ "Image": "sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352", │"
"│ "ResolvConfPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c853│"
"│ "HostnamePath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358│"
"│ "HostsPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456│"
"│ "LogPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc│"
"│ "Name": "/postgres", │"
"│ "RestartCount": 0, │"
"│ "Driver": "overlay2", │"
"│ "Platform": "linux", │"
"│ "MountLabel": "", │"
"│ "ProcessLabel": "", │"
"│ "AppArmorProfile": "", │"
"│ "HostConfig": { │"
"│ "CpuShares": 0, │"
"│ "Memory": 1073741824, │"
"│ "CgroupParent": "", │"
"│ "BlkioWeight": 0, │"
"│ "CpuPeriod": 0, │"
"│ "CpuQuota": 0, │"
"│ "CpuRealtimePeriod": 0, │"
"│ "CpuRealtimeRuntime": 0, │"
"│ "CpusetCpus": "", │"
"│ "CpusetMems": "", │"
"│ "MemoryReservation": 0, │"
"│ "MemorySwap": 2147483648, │"
"│ "NanoCpus": 0, │"
"│ "OomKillDisable": false, │"
"│ "CpuCount": 0, │"
"│ "CpuPercent": 0, │"
"╰──────────────────────────────────── 0/158 ↓ 0/972 → ────────────────────────────────────╯"
@@ -0,0 +1,54 @@
---
source: src/ui/draw_blocks/inspect.rs
expression: setup.terminal.backend()
---
"╭────────────────── inspecting: postgres 0bdea642 - c or Esc or 4 or 5 to exit ───────────────────╮"
"│ "Id": "0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7", │"
"│ "Created": "2026-01-23T22:20:19.927967311Z", │"
"│ "Path": "docker-entrypoint.sh", │"
"│ "Args": [ │"
"│ "postgres" │"
"│ ], │"
"│ "State": { │"
"│ "Status": "running", │"
"│ "Running": true, │"
"│ "Paused": false, │"
"│ "Restarting": false, │"
"│ "OOMKilled": false, │"
"│ "Dead": false, │"
"│ "Pid": 782, │"
"│ "ExitCode": 0, │"
"│ "Error": "", │"
"│ "StartedAt": "2026-01-30T08:09:01.574885915Z", │"
"│ "FinishedAt": "2026-01-30T08:09:01.180567927Z" │"
"│ }, │"
"│ "Image": "sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352", │"
"│ "ResolvConfPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c853│"
"│ "HostnamePath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358│"
"│ "HostsPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456│"
"│ "LogPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc│"
"│ "Name": "/postgres", │"
"│ "RestartCount": 0, │"
"│ "Driver": "overlay2", │"
"│ "Platform": "linux", │"
"│ "MountLabel": "", │"
"│ "ProcessLabel": "", │"
"│ "AppArmorProfile": "", │"
"│ "HostConfig": { │"
"│ "CpuShares": 0, │"
"│ "Memory": 1073741824, │"
"│ "CgroupParent": "", │"
"│ "BlkioWeight": 0, │"
"│ "CpuPeriod": 0, │"
"│ "CpuQuota": 0, │"
"│ "CpuRealtimePeriod": 0, │"
"│ "CpuRealtimeRuntime": 0, │"
"│ "CpusetCpus": "", │"
"│ "CpusetMems": "", │"
"│ "MemoryReservation": 0, │"
"│ "MemorySwap": 2147483648, │"
"│ "NanoCpus": 0, │"
"│ "OomKillDisable": false, │"
"│ "CpuCount": 0, │"
"│ "CpuPercent": 0, │"
"╰──────────────────────────────────── 0/158 ↓ 0/972 → ────────────────────────────────────╯"
@@ -0,0 +1,54 @@
---
source: src/ui/draw_blocks/inspect.rs
expression: setup.terminal.backend()
---
"╭───────────────────── inspecting: postgres 0bdea642 - c or Esc or i to exit ─────────────────────╮"
"│ "Id": "0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7", │"
"│ "Created": "2026-01-23T22:20:19.927967311Z", │"
"│ "Path": "docker-entrypoint.sh", │"
"│ "Args": [ │"
"│ "postgres" │"
"│ ], │"
"│ "State": { │"
"│ "Status": "running", │"
"│ "Running": true, │"
"│ "Paused": false, │"
"│ "Restarting": false, │"
"│ "OOMKilled": false, │"
"│ "Dead": false, │"
"│ "Pid": 782, │"
"│ "ExitCode": 0, │"
"│ "Error": "", │"
"│ "StartedAt": "2026-01-30T08:09:01.574885915Z", │"
"│ "FinishedAt": "2026-01-30T08:09:01.180567927Z" │"
"│ }, │"
"│ "Image": "sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352", │"
"│ "ResolvConfPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c853│"
"│ "HostnamePath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358│"
"│ "HostsPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456│"
"│ "LogPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc│"
"│ "Name": "/postgres", │"
"│ "RestartCount": 0, │"
"│ "Driver": "overlay2", │"
"│ "Platform": "linux", │"
"│ "MountLabel": "", │"
"│ "ProcessLabel": "", │"
"│ "AppArmorProfile": "", │"
"│ "HostConfig": { │"
"│ "CpuShares": 0, │"
"│ "Memory": 1073741824, │"
"│ "CgroupParent": "", │"
"│ "BlkioWeight": 0, │"
"│ "CpuPeriod": 0, │"
"│ "CpuQuota": 0, │"
"│ "CpuRealtimePeriod": 0, │"
"│ "CpuRealtimeRuntime": 0, │"
"│ "CpusetCpus": "", │"
"│ "CpusetMems": "", │"
"│ "MemoryReservation": 0, │"
"│ "MemorySwap": 2147483648, │"
"│ "NanoCpus": 0, │"
"│ "OomKillDisable": false, │"
"│ "CpuCount": 0, │"
"│ "CpuPercent": 0, │"
"╰──────────────────────────────────── 0/158 ↓ 0/972 → ────────────────────────────────────╯"
@@ -0,0 +1,54 @@
---
source: src/ui/draw_blocks/inspect.rs
expression: setup.terminal.backend()
---
"╭───────────────────── inspecting: postgres 0bdea642 - c or Esc or i to exit ─────────────────────╮"
"│rting": false, │"
"│lled": false, │"
"│: false, │"
"│ 782, │"
"│ode": 0, │"
"│": "", │"
"│edAt": "2026-01-30T08:09:01.574885915Z", │"
"│hedAt": "2026-01-30T08:09:01.180567927Z" │"
"│ │"
"│ "sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352", │"
"│onfPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb│"
"│ePath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60│"
"│th": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7/│"
"│": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7/0b│"
"│"/postgres", │"
"│Count": 0, │"
"│: "overlay2", │"
"│m": "linux", │"
"│bel": "", │"
"│Label": "", │"
"│rProfile": "", │"
"│fig": { │"
"│ares": 0, │"
"│y": 1073741824, │"
"│pParent": "", │"
"│Weight": 0, │"
"│riod": 0, │"
"│ota": 0, │"
"│altimePeriod": 0, │"
"│altimeRuntime": 0, │"
"│tCpus": "", │"
"│tMems": "", │"
"│yReservation": 0, │"
"│ySwap": 2147483648, │"
"│pus": 0, │"
"│llDisable": false, │"
"│unt": 0, │"
"│rcent": 0, │"
"│imumIOps": 0, │"
"│imumBandwidth": 0, │"
"│inerIDFile": "", │"
"│nfig": { │"
"│e": "json-file", │"
"│fig": {} │"
"│ │"
"│rkMode": "oxker-examaple-net", │"
"│indings": {}, │"
"│rtPolicy": { │"
"╰──────────────────────────────────── ↑ 10/158 ↓ ← 10/972 → ────────────────────────────────────╯"
@@ -0,0 +1,54 @@
---
source: src/ui/draw_blocks/inspect.rs
expression: setup.terminal.backend()
---
"╭───────────────────── inspecting: postgres 0bdea642 - c or Esc or i to exit ─────────────────────╮"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"│ │"
"╰──────────────────────────────────── ↑ 158/158 ← 972/972 ────────────────────────────────────╯"
@@ -4,41 +4,41 @@ 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 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 ││ │"
"╰────────────────────────────────────│ ,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 ( Home End ) scroll vertically │ │"
"│ │ ( ← → ) horizontal scroll across logs │ │"
"│ │ ( ctrl ) increase scroll speed, used in conjunction scroll keys │ │"
"│ │ ( 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 │ │"
"│ │ ( F1 ) or ( / ) enter filter mode │"
"│ │ ( # ) enter log search mode │"
"│ │ ( 0 ) stop sort │"
"│ │ ( 1 - 9 ) sort by header - or click header │"
"╰────────────────────────────────────│ ( - = ) change log section height │────────────────────────────────────╯"
"╭───────────────────────── cpu 03.00%│ ( \ ) toggle log section visibility │──────╮╭────────── ports ───────────╮"
"│10.00%│ • │ ( esc ) close dialog │ ││ ip private public│"
"│ │ ••• │ ( q ) quit at any time ││ 8001 │"
"│ │ • • ││127.0.0.1 8003 8003│"
"│ │ • • currently an early work in progress, all and any input appreciated │ ││ │"
"│ │ •• • │ https://github.com/mrjackwills/oxker ││ │"
"│ │ • • • ││ │"
"│ │•• •• ╰────────────────────────────────────────────────────────────────────────────────────╯ ││ │"
"│⚪ 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 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │"
"│ ││ delete │"
"│ ││ │"
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯"
"╭ Logs 3/3 - container_╭ 0.00.000 ──────────────────────────────────────────────────────────────────────────────────────────────────────╮──────────────────────╮"
"│ line 1 88 │"
"│ line 2 88 │ │"
"│▶ line 3 ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYb │ │"
"│ │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' " │ │"
"│ │ 8b d8 )888( 8888( 8PP""""""" 88 │ │"
"│ │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ │"
"│ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ │"
"│ a work in progress, all and any input appreciated │"
"│ A simple tui to view & control docker containers https://github.com/mrjackwills/oxker │ │"
"│ │"
"│ Keymap │"
"│ │ q quit c Esc close dialog │ │"
"│ │ Down Up j k Home End scroll vertically Left Right scroll horizontally │ │"
"│ │ Control increase scroll speed Enter send docker command │"
"│ │ e exec into a container i container inspect mode │"
"│ │ / F1 filter mode # log search mode │"
"│ │ h toggle this panel f force clear screen and redraw │ │"
"│ │ - = change log section height \ toggle of section visibility │ │"
"│ │ 1 ~ 9 sort by header - or click header 0 stop sort │ │"
"│ │ Tab Back Tab change panel m toggle mouse capture - allows text selection │ │"
"╰──────────────────────│ s save logs to file │──────────────────────╯"
"╭────────────────────── ──── ports ───────────╮"
"│10.00%│ • ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ip private public│"
"│ │ ••• ││ │ ••• ││ 8001 │"
"│ │ • • ││ │ • • ││127.0.0.1 8003 8003│"
"│ │ • • ││ │ • • ││ │"
"│ │ •• • ││ │"
"│ │ • • • ││ │ • •• ││ │"
"│ │•• •• ││ │•• •• ││ │"
"│ │ ││ │ ││ │"
"╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯"
+56 -1
View File
@@ -9,7 +9,7 @@ use tokio::task::JoinHandle;
use uuid::Uuid;
use crate::{
app_data::{AppData, ContainerId, Header},
app_data::{AppData, ContainerId, Header, ScrollDirection},
exec::ExecMode,
};
@@ -169,10 +169,17 @@ pub enum Status {
Filter,
Help,
Init,
Inspect,
Logs,
SearchLogs,
}
#[derive(Debug, Default, Copy, Clone, Hash, PartialEq, Eq)]
pub struct ScrollOffset {
pub x: usize,
pub y: usize,
}
/// Global gui_state, stored in an Arc<Mutex>
#[derive(Debug)]
pub struct GuiState {
@@ -190,6 +197,8 @@ pub struct GuiState {
selected_panel: SelectablePanel,
screen_width: u16,
show_logs: bool,
inspect_offset: ScrollOffset,
inspect_offset_max: ScrollOffset,
status: HashSet<Status>,
pub info_box_text: Option<(String, Instant)>,
}
@@ -203,6 +212,8 @@ impl GuiState {
intersect_heading: HashMap::new(),
intersect_help: None,
intersect_panel: HashMap::new(),
inspect_offset: ScrollOffset::default(),
inspect_offset_max: ScrollOffset::default(),
loading_handle: None,
loading_index: 0,
loading_set: HashSet::new(),
@@ -235,6 +246,50 @@ impl GuiState {
}
}
pub fn set_inspect_offset(&mut self, sd: &ScrollDirection) {
match sd {
ScrollDirection::Up => self.inspect_offset.y = self.inspect_offset.y.saturating_sub(1),
ScrollDirection::Down => {
self.inspect_offset.y = self
.inspect_offset
.y
.saturating_add(1)
.min(self.inspect_offset_max.y)
}
ScrollDirection::Left => {
self.inspect_offset.x = self.inspect_offset.x.saturating_sub(1)
}
ScrollDirection::Right => {
self.inspect_offset.x = self
.inspect_offset
.x
.saturating_add(1)
.min(self.inspect_offset_max.x)
}
}
self.rerender.update_draw();
}
pub fn get_inspect_offset(&self) -> ScrollOffset {
self.inspect_offset
}
pub fn set_inspect_offset_max(&mut self, offset: ScrollOffset) {
self.inspect_offset_max = offset
}
pub fn set_inspect_offset_y_to_max(&mut self) {
self.inspect_offset.y = self.inspect_offset_max.y;
self.rerender.update_draw();
}
pub fn clear_inspect_offset(&mut self) {
self.inspect_offset.x = 0;
self.inspect_offset.y = 0;
self.inspect_offset_max = ScrollOffset::default();
self.rerender.update_draw();
}
/// Set the screen width, used for offset char calculations
pub const fn set_screen_width(&mut self, width: u16) {
self.screen_width = width;
+96 -95
View File
@@ -35,7 +35,6 @@ use crate::{
},
app_error::AppError,
config::{AppColors, Keymap},
exec::TerminalSize,
input_handler::InputMessages,
};
@@ -184,7 +183,8 @@ impl Ui {
if let Some(mode) = exec_mode {
self.reset_terminal().ok();
self.terminal.clear().ok();
if let Err(e) = mode.run(TerminalSize::new(&self.terminal)).await {
if let Err(e) = mode.run(self.terminal.size().ok()).await {
self.app_data
.lock()
.set_error(e, &self.gui_state, Status::Error);
@@ -359,113 +359,114 @@ fn draw_frame(
let contains_filter = fd.status.contains(&Status::Filter);
let contains_search_logs = fd.status.contains(&Status::SearchLogs);
let whole_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(if contains_filter || contains_search_logs {
vec![Constraint::Max(1), Constraint::Min(1), Constraint::Max(1)]
} else {
vec![Constraint::Max(1), Constraint::Min(1)]
})
.split(f.area());
let contains_inspect = fd.status.contains(&Status::Inspect);
draw_blocks::headers::draw(whole_layout[0], colors, f, fd, gui_state, keymap);
let inspect_data = app_data.lock().get_inspect_data();
if contains_inspect && let Some(inspect_data) = inspect_data {
draw_blocks::inspect::draw(f, colors, inspect_data, gui_state, keymap);
} else {
let whole_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(if contains_filter || contains_search_logs {
vec![Constraint::Max(1), Constraint::Min(1), Constraint::Max(1)]
} else {
vec![Constraint::Max(1), Constraint::Min(1)]
})
.split(f.area());
if let Some(rect) = whole_layout.get(2) {
if contains_filter {
draw_blocks::filter::draw(*rect, colors, f, fd);
} else {
draw_blocks::search_logs::draw(*rect, colors, f, fd, keymap);
draw_blocks::headers::draw(whole_layout[0], colors, f, fd, gui_state, keymap);
if let Some(rect) = whole_layout.get(2) {
if contains_filter {
draw_blocks::filter::draw(*rect, colors, f, fd);
} else {
draw_blocks::search_logs::draw(*rect, colors, f, fd, keymap);
}
}
}
let upper_main = Layout::default()
.direction(Direction::Vertical)
.constraints(if fd.has_containers {
vec![Constraint::Percentage(75), Constraint::Percentage(25)]
} else {
vec![Constraint::Percentage(100), Constraint::Percentage(0)]
})
.split(whole_layout[1]);
let upper_main = Layout::default()
.direction(Direction::Vertical)
.constraints(if fd.has_containers {
vec![Constraint::Percentage(75), Constraint::Percentage(25)]
} else {
vec![Constraint::Percentage(100), Constraint::Percentage(0)]
})
.split(whole_layout[1]);
let containers_logs_section = Layout::default()
.direction(Direction::Vertical)
.constraints(if fd.show_logs {
vec![Constraint::Min(6), Constraint::Percentage(fd.log_height)]
} else {
vec![Constraint::Percentage(100)]
})
.split(upper_main[0]);
let containers_logs_section = Layout::default()
.direction(Direction::Vertical)
.constraints(if fd.show_logs {
vec![Constraint::Min(6), Constraint::Percentage(fd.log_height)]
} else {
vec![Constraint::Percentage(100)]
})
.split(upper_main[0]);
// Containers + docker commands
let containers_commands = Layout::default()
.direction(Direction::Horizontal)
.constraints(if fd.has_containers {
vec![Constraint::Percentage(90), Constraint::Percentage(10)]
} else {
vec![Constraint::Percentage(100)]
})
.split(containers_logs_section[0]);
draw_blocks::containers::draw(app_data, containers_commands[0], colors, f, fd, gui_state);
if fd.show_logs {
draw_blocks::logs::draw(
app_data,
containers_logs_section[1],
colors,
f,
fd,
gui_state,
);
}
if let Some(id) = fd.delete_confirm.as_ref() {
app_data.lock().get_container_name_by_id(id).map_or_else(
|| {
// If a container is deleted outside of oxker but whilst the Delete Confirm dialog is open, it can get caught in kind of a dead lock situation
// so if in that unique situation, just clear the delete_container id
gui_state.lock().set_delete_container(None);
},
|name| {
draw_blocks::delete_confirm::draw(colors, f, gui_state, keymap, name);
},
);
}
// only draw commands + charts if there are containers
if let Some(rect) = containers_commands.get(1) {
draw_blocks::commands::draw(app_data, *rect, colors, f, fd, gui_state);
// Can calculate the max string length here, and then use that to keep the ports section as small as possible (+4 for some padding + border)
let ports_len =
u16::try_from(fd.port_max_lens.0 + fd.port_max_lens.1 + fd.port_max_lens.2 + 2)
.unwrap_or(26);
let lower = Layout::default()
// Containers + docker commands
let containers_commands = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(1), Constraint::Max(ports_len)])
.split(upper_main[1]);
.constraints(if fd.has_containers {
vec![Constraint::Percentage(90), Constraint::Percentage(10)]
} else {
vec![Constraint::Percentage(100)]
})
.split(containers_logs_section[0]);
draw_blocks::charts::draw(lower[0], colors, f, fd);
draw_blocks::ports::draw(lower[1], colors, f, fd);
draw_blocks::containers::draw(app_data, containers_commands[0], colors, f, fd, gui_state);
if fd.show_logs {
draw_blocks::logs::draw(
app_data,
containers_logs_section[1],
colors,
f,
fd,
gui_state,
);
}
if let Some(id) = fd.delete_confirm.as_ref() {
app_data.lock().get_container_name_by_id(id).map_or_else(
|| {
// If a container is deleted outside of oxker but whilst the Delete Confirm dialog is open, it can get caught in kind of a dead lock situation
// so if in that unique situation, just clear the delete_container id
gui_state.lock().set_delete_container(None);
},
|name| {
draw_blocks::delete_confirm::draw(colors, f, gui_state, keymap, name);
},
);
}
// only draw commands + charts if there are containers
if let Some(rect) = containers_commands.get(1) {
draw_blocks::commands::draw(app_data, *rect, colors, f, fd, gui_state);
// Can calculate the max string length here, and then use that to keep the ports section as small as possible (+4 for some padding + border)
let ports_len =
u16::try_from(fd.port_max_lens.0 + fd.port_max_lens.1 + fd.port_max_lens.2 + 2)
.unwrap_or(26);
let lower = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(1), Constraint::Max(ports_len)])
.split(upper_main[1]);
draw_blocks::charts::draw(lower[0], colors, f, fd);
draw_blocks::ports::draw(lower[1], colors, f, fd);
}
// Check if error, and show popup if so
if fd.status.contains(&Status::Help) {
let config = app_data.lock().config.clone();
draw_blocks::help::draw(&config, f);
}
}
if let Some((text, instant)) = fd.info_text.as_ref() {
draw_blocks::info::draw(colors, f, gui_state, instant, text.to_owned());
}
// Check if error, and show popup if so
if fd.status.contains(&Status::Help) {
let tz = app_data.lock().config.timezone.clone();
draw_blocks::help::draw(
colors,
f,
keymap,
app_data.lock().config.show_timestamp,
tz.as_ref(),
);
}
if let Some(error) = fd.has_error.as_ref() {
draw_blocks::error::draw(colors, error, f, None, keymap, None);
}