From d6cd9123d5a806821a880172d49b6c54603b8157 Mon Sep 17 00:00:00 2001 From: Vitali Pikulik Date: Wed, 10 Dec 2025 15:42:01 +0100 Subject: [PATCH 1/8] Tree integration --- Cargo.lock | 12 ++++++++++++ Cargo.toml | 11 ++++++----- src/pages/describe.rs | 42 +++++++++++++++++++++++++++++------------- 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 09161b5..4df4630 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -809,6 +809,7 @@ dependencies = [ "tracing-error", "tracing-subscriber", "tui-big-text", + "tui-tree-widget", "ureq", ] @@ -3056,6 +3057,17 @@ dependencies = [ "ratatui-core", ] +[[package]] +name = "tui-tree-widget" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deca119555009eee2e0cfb9c020f39f632444dc4579918d5fc009d51d75dff92" +dependencies = [ + "ratatui-core", + "ratatui-widgets", + "unicode-width", +] + [[package]] name = "typenum" version = "1.19.0" diff --git a/Cargo.toml b/Cargo.toml index d02e987..8077b48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,9 +31,9 @@ lazy_static = "1.5.0" serde = "1.0.228" serde_yml = "0.0.12" tokio = { version = "1.49.0", features = [ - "rt-multi-thread", - "macros", - "process", + "rt-multi-thread", + "macros", + "process", ] } tracing = "0.1.44" tracing-error = "0.2.1" @@ -44,10 +44,11 @@ ureq = { version = "3.1.4", features = ["json"] } ansi-to-tui = "8.0.1" ratatui-macros = "0.7.0" ratatui = { version = "0.30.0", features = [ - "serde", - "unstable-rendered-line-info", + "serde", + "unstable-rendered-line-info", ] } tui-big-text = "0.8.1" +tui-tree-widget = "0.24.0" [dev-dependencies] tempfile = "3.24.0" diff --git a/src/pages/describe.rs b/src/pages/describe.rs index 0e8fc69..a235cd1 100644 --- a/src/pages/describe.rs +++ b/src/pages/describe.rs @@ -4,11 +4,12 @@ use std::sync::{Arc, Mutex}; use bollard::Docker; use color_eyre::eyre::{Result, bail}; -use ratatui::style::Style; +use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span}; -use ratatui::widgets::Paragraph; +use ratatui::widgets::{Scrollbar, ScrollbarOrientation}; use ratatui::{Frame, layout::Rect}; use tokio::sync::mpsc::Sender; +use tui_tree_widget::{Tree, TreeItem, TreeState}; use crate::config::Config; use crate::context::AppContext; @@ -38,6 +39,7 @@ pub struct DescribeContainer { cx: Option, page_help: Arc>, scroll: u16, + tree_state: TreeState, } impl DescribeContainer { @@ -53,6 +55,7 @@ impl DescribeContainer { cx: None, page_help: Arc::new(Mutex::new(page_help)), scroll: 0, + tree_state: TreeState::default(), } } @@ -157,7 +160,7 @@ impl Component for DescribeContainer { return; } let container_summary = self.thing_summary.as_ref().unwrap(); - let lines: Vec = container_summary + let info: Vec> = container_summary .iter() .map(|l| { let l = l.clone(); @@ -168,21 +171,34 @@ impl Component for DescribeContainer { let key_style = Style::default().fg(self.config.theme.footer()); - Line::from(vec![ + let line = Line::from(vec![ Span::from(key.clone()).style(key_style), Span::from(":"), Span::from(val.clone()), - ]) + ]); + TreeItem::new_leaf(format!("{key}:{val}"), line) }) .collect(); - let paragraph = Paragraph::new(lines); - - let n_lines = paragraph.line_count(area.width) as u16; - - let scroll = self.resolve_scroll(&area.height, &n_lines); - - let paragraph = paragraph.scroll((scroll, 0)); - f.render_widget(paragraph, area) + let tree = vec![TreeItem::new("info".to_string(), Line::from("Info"), info).unwrap()]; + + let widget = Tree::new(tree.as_slice()) + .expect("all item identifiers are unique") + .experimental_scrollbar(Some( + Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(None) + .track_symbol(None) + .end_symbol(None), + )) + .highlight_style( + Style::new() + .fg(Color::Black) + .bg(Color::LightGreen) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(">> "); + self.tree_state.open(vec!["info".to_string()]); + + f.render_stateful_widget(widget, area, &mut self.tree_state); } } From 499ba99b037ab24761bb21733a60983955b89cb5 Mon Sep 17 00:00:00 2001 From: Vitali Pikulik Date: Wed, 7 Jan 2026 13:27:58 +0100 Subject: [PATCH 2/8] Use tree on describe page --- Cargo.lock | 7 +-- Cargo.toml | 1 + src/docker/container.rs | 23 ++++++--- src/docker/image.rs | 23 +++++---- src/docker/network.rs | 31 ++++++++---- src/docker/traits.rs | 46 ++++++++++++++++- src/docker/volume.rs | 48 +++++++++++++++--- src/pages/describe.rs | 106 +++++++++++++++++++++++----------------- 8 files changed, 203 insertions(+), 82 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4df4630..ca7391a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -811,6 +811,7 @@ dependencies = [ "tui-big-text", "tui-tree-widget", "ureq", + "uuid", ] [[package]] @@ -3185,14 +3186,14 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "atomic", "getrandom 0.3.3", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml index 8077b48..eb8f244 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ tracing = "0.1.44" tracing-error = "0.2.1" tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } ureq = { version = "3.1.4", features = ["json"] } +uuid = { version = "1.19.0", features = ["v4"] } # Ratatui Dependencies; these often need to be updated in lockstep ansi-to-tui = "8.0.1" diff --git a/src/docker/container.rs b/src/docker/container.rs index 0090513..60b3c84 100644 --- a/src/docker/container.rs +++ b/src/docker/container.rs @@ -14,6 +14,8 @@ use tokio::process::Command; use bollard::secret::ContainerSummary; +use crate::docker::traits::DescribeSection; + use super::traits::Describe; #[derive(Debug, Clone, PartialEq, Serialize)] @@ -172,14 +174,19 @@ impl Describe for DockerContainer { fn get_name(&self) -> String { format!("container: {}", self.names) } - fn describe(&self) -> Result> { - let summary = match serde_yml::to_string(&self) { - Ok(s) => s, - Err(_) => { - bail!("failed to parse container summary") - } - }; - Ok(summary.lines().map(String::from).collect()) + fn describe(&self) -> Result> { + let mut summary = DescribeSection::new("Summary"); + summary + .item("ID", &self.id) + .item("Image", &self.image) + .item("Image ID", &self.image_id) + .item("Command", &self.command) + .item("Created", &self.created) + .item("Status", &self.status) + .item("Ports", &self.ports) + .item("Names", &self.names) + .item("Running", self.running); + Ok(vec![summary]) } } diff --git a/src/docker/image.rs b/src/docker/image.rs index 822e897..d503d04 100644 --- a/src/docker/image.rs +++ b/src/docker/image.rs @@ -2,7 +2,7 @@ use bollard::query_parameters::{ListImagesOptionsBuilder, RemoveImageOptionsBuil use byte_unit::{Byte, UnitType}; use chrono::Local; use chrono::prelude::DateTime; -use color_eyre::eyre::{Context, Result, bail}; +use color_eyre::eyre::{Context, Result}; use itertools::Itertools; use serde::Serialize; use std::collections::HashMap; @@ -10,6 +10,8 @@ use std::time::{Duration, UNIX_EPOCH}; use bollard::secret::ImageSummary; +use crate::docker::traits::DescribeSection; + use super::traits::Describe; #[derive(Debug, Clone, Eq, PartialEq, Serialize)] @@ -112,13 +114,16 @@ impl Describe for DockerImage { fn get_name(&self) -> String { format!("image: {}", self.name) } - fn describe(&self) -> Result> { - let summary = match serde_yml::to_string(&self) { - Ok(s) => s, - Err(_) => { - bail!("failed to parse image summary") - } - }; - Ok(summary.lines().map(String::from).collect()) + fn describe(&self) -> Result> { + let mut summary = DescribeSection::new("Summary"); + summary + .item("ID", &self.id) + .item("Name", &self.name) + .item("Tag", &self.tag) + .item("Created", &self.created) + .item("Size", &self.size) + .item("Tags", self.tags.join(", ")) + .item("Digests", self.digests.join(", ")); + Ok(vec![summary]) } } diff --git a/src/docker/network.rs b/src/docker/network.rs index b210cf8..b2ce2a9 100644 --- a/src/docker/network.rs +++ b/src/docker/network.rs @@ -1,8 +1,10 @@ use bollard::query_parameters::ListNetworksOptionsBuilder; use bollard::secret::Network; -use color_eyre::eyre::{Result, bail}; +use color_eyre::eyre::Result; use serde::Serialize; +use crate::docker::traits::DescribeSection; + use super::traits::Describe; #[derive(Debug, Clone, PartialEq, Serialize)] @@ -53,13 +55,24 @@ impl Describe for DockerNetwork { self.name.clone() } - fn describe(&self) -> Result> { - let summary = match serde_yml::to_string(&self) { - Ok(s) => s, - Err(_) => { - bail!("failed to parse container summary") - } - }; - Ok(summary.lines().map(String::from).collect()) + fn describe(&self) -> Result> { + let mut summary = DescribeSection::new("Summary"); + summary + .item("ID", &self.id) + .item("Name", &self.name) + .item("Driver", &self.driver) + .item("Created At", &self.created_at) + .item("Scope", &self.scope) + .item( + "Internal", + self.internal.map(|v| v.to_string()).unwrap_or("N/A".into()), + ) + .item( + "Attachable", + self.attachable + .map(|v| v.to_string()) + .unwrap_or("N/A".into()), + ); + Ok(vec![summary]) } } diff --git a/src/docker/traits.rs b/src/docker/traits.rs index df982ee..c8bdcfd 100644 --- a/src/docker/traits.rs +++ b/src/docker/traits.rs @@ -2,6 +2,7 @@ use std::fmt; use color_eyre::eyre::Result; use dyn_clone::DynClone; +use uuid::Uuid; /// Provides an interface to describe the contents of the implementing /// struct in a human readable format. @@ -13,7 +14,50 @@ pub trait Describe: fmt::Debug + Send + Sync + DynClone { /// Get a human readable name of the resource being described fn get_name(&self) -> String; /// Get a human readable description of the resource being described - fn describe(&self) -> Result>; + fn describe(&self) -> Result>; } dyn_clone::clone_trait_object!(Describe); + +#[derive(Debug, PartialEq, Eq)] +pub struct DescribeSection { + pub(crate) id: Uuid, + pub(crate) name: String, + pub(crate) items: Vec, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct DescribeItem { + pub(crate) id: Uuid, + pub(crate) name: String, + pub(crate) value: String, +} + +impl DescribeItem { + pub(crate) fn new(name: N, value: V) -> Self { + Self { + id: Uuid::new_v4(), + name: name.to_string(), + value: value.to_string(), + } + } +} + +impl DescribeSection { + pub(crate) fn new(name: N) -> Self { + Self { + id: Uuid::new_v4(), + name: name.to_string(), + items: vec![], + } + } + + pub(crate) fn push(&mut self, item: DescribeItem) -> &mut Self { + self.items.push(item); + self + } + + pub(crate) fn item(&mut self, name: N, value: V) -> &mut Self { + self.push(DescribeItem::new(name, value)) + } +} diff --git a/src/docker/volume.rs b/src/docker/volume.rs index 271084a..c0a225b 100644 --- a/src/docker/volume.rs +++ b/src/docker/volume.rs @@ -5,6 +5,8 @@ use color_eyre::eyre::{Result, bail}; use serde::Serialize; use std::collections::HashMap; +use crate::docker::traits::DescribeSection; + use super::traits::Describe; #[derive(Debug, Clone, PartialEq, Serialize)] @@ -88,13 +90,43 @@ impl Describe for DockerVolume { self.name.clone() } - fn describe(&self) -> Result> { - let summary = match serde_yml::to_string(&self) { - Ok(s) => s, - Err(_) => { - bail!("failed to parse container summary") - } - }; - Ok(summary.lines().map(String::from).collect()) + fn describe(&self) -> Result> { + let mut summary = DescribeSection::new("Summary"); + summary + .item("Name", &self.name) + .item("Driver", &self.driver) + .item("Mountpoint", &self.mountpoint) + .item("Created At", self.created_at.as_deref().unwrap_or("N/A")) + .item( + "Labels", + if self.labels.is_empty() { + "N/A".into() + } else { + format!("{}", self.labels.len()) + }, + ) + .item( + "Scope", + self.scope + .as_ref() + .map(|v| format!("{v}")) + .unwrap_or("N/A".into()), + ) + .item( + "Options", + if self.options.is_empty() { + "N/A".into() + } else { + format!("{}", self.options.len()) + }, + ) + .item( + "Reference Count", + self.ref_count + .map(|v| v.to_string()) + .unwrap_or("N/A".into()), + ) + .item("Size", self.size.as_deref().unwrap_or("N/A")); + Ok(vec![summary]) } } diff --git a/src/pages/describe.rs b/src/pages/describe.rs index a235cd1..e504167 100644 --- a/src/pages/describe.rs +++ b/src/pages/describe.rs @@ -4,16 +4,18 @@ use std::sync::{Arc, Mutex}; use bollard::Docker; use color_eyre::eyre::{Result, bail}; +use itertools::Itertools; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Scrollbar, ScrollbarOrientation}; use ratatui::{Frame, layout::Rect}; use tokio::sync::mpsc::Sender; use tui_tree_widget::{Tree, TreeItem, TreeState}; +use uuid::Uuid; use crate::config::Config; use crate::context::AppContext; -use crate::docker::traits::Describe; +use crate::docker::traits::{Describe, DescribeSection}; use crate::traits::Close; use crate::{ components::help::{PageHelp, PageHelpBuilder}, @@ -34,12 +36,12 @@ pub struct DescribeContainer { _docker: Docker, config: Arc, thing: Option>, - thing_summary: Option>, + thing_summary: Option>, tx: Sender>, cx: Option, page_help: Arc>, scroll: u16, - tree_state: TreeState, + tree_state: TreeState, } impl DescribeContainer { @@ -154,51 +156,67 @@ impl Page for DescribeContainer { #[async_trait::async_trait] impl Close for DescribeContainer {} +fn section_to_tree_item<'a>( + state: &mut TreeState, + section: &'a DescribeSection, + section_style: &Style, + key_style: &Style, +) -> TreeItem<'a, Uuid> { + let items: Vec> = section + .items + .iter() + .map(|item| { + let line = Line::from(vec![ + Span::from(&item.name).style(key_style.clone()), + Span::from(":"), + Span::from(&item.value), + ]); + TreeItem::new_leaf(item.id, line) + }) + .collect(); + + let item = TreeItem::new( + section.id, + Span::from(§ion.name).style(*section_style), + items, + ) + .unwrap(); + state.open(vec![section.id]); + item +} + impl Component for DescribeContainer { fn draw(&mut self, f: &mut Frame<'_>, area: Rect) { if self.thing_summary.is_none() { return; } - let container_summary = self.thing_summary.as_ref().unwrap(); - let info: Vec> = container_summary - .iter() - .map(|l| { - let l = l.clone(); - - let mut row = l.splitn(2, ':'); - let key = String::from(row.next().unwrap_or("")); - let val = String::from(row.next().unwrap_or("")); - - let key_style = Style::default().fg(self.config.theme.footer()); - - let line = Line::from(vec![ - Span::from(key.clone()).style(key_style), - Span::from(":"), - Span::from(val.clone()), - ]); - TreeItem::new_leaf(format!("{key}:{val}"), line) - }) - .collect(); - - let tree = vec![TreeItem::new("info".to_string(), Line::from("Info"), info).unwrap()]; - - let widget = Tree::new(tree.as_slice()) - .expect("all item identifiers are unique") - .experimental_scrollbar(Some( - Scrollbar::new(ScrollbarOrientation::VerticalRight) - .begin_symbol(None) - .track_symbol(None) - .end_symbol(None), - )) - .highlight_style( - Style::new() - .fg(Color::Black) - .bg(Color::LightGreen) - .add_modifier(Modifier::BOLD), - ) - .highlight_symbol(">> "); - self.tree_state.open(vec!["info".to_string()]); - - f.render_stateful_widget(widget, area, &mut self.tree_state); + if let Some(summary) = &self.thing_summary { + let section_style = Style::default().fg(self.config.theme.footer()); + let key_style = Style::default().fg(self.config.theme.footer()); + let tree = summary + .iter() + .map(|section| { + section_to_tree_item(&mut self.tree_state, section, §ion_style, &key_style) + }) + .collect_vec(); + + let widget = Tree::new(tree.as_slice()) + .expect("all item identifiers are unique") + .experimental_scrollbar(Some( + Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(None) + .track_symbol(None) + .end_symbol(None), + )) + .highlight_style( + Style::new() + .fg(Color::Black) + .bg(Color::LightGreen) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(">> "); + + f.render_stateful_widget(widget, area, &mut self.tree_state); + } } } From f4bb07b54d58c02a9ad1c4ca64c20fe5e2067565 Mon Sep 17 00:00:00 2001 From: Vitali Pikulik Date: Wed, 7 Jan 2026 13:32:11 +0100 Subject: [PATCH 3/8] Cleanup --- src/docker/container.rs | 3 +-- src/docker/image.rs | 3 +-- src/docker/network.rs | 3 +-- src/docker/traits.rs | 12 ++++-------- src/docker/volume.rs | 3 +-- 5 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/docker/container.rs b/src/docker/container.rs index 60b3c84..2c592a0 100644 --- a/src/docker/container.rs +++ b/src/docker/container.rs @@ -5,7 +5,6 @@ use bollard::query_parameters::{ use chrono::Local; use chrono::prelude::DateTime; use color_eyre::eyre::{Context, Result, bail}; -use serde::Serialize; use std::{ collections::HashMap, time::{Duration, UNIX_EPOCH}, @@ -18,7 +17,7 @@ use crate::docker::traits::DescribeSection; use super::traits::Describe; -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] pub struct DockerContainer { pub id: String, pub image_id: String, diff --git a/src/docker/image.rs b/src/docker/image.rs index d503d04..a199882 100644 --- a/src/docker/image.rs +++ b/src/docker/image.rs @@ -4,7 +4,6 @@ use chrono::Local; use chrono::prelude::DateTime; use color_eyre::eyre::{Context, Result}; use itertools::Itertools; -use serde::Serialize; use std::collections::HashMap; use std::time::{Duration, UNIX_EPOCH}; @@ -14,7 +13,7 @@ use crate::docker::traits::DescribeSection; use super::traits::Describe; -#[derive(Debug, Clone, Eq, PartialEq, Serialize)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct DockerImage { pub id: String, pub name: String, diff --git a/src/docker/network.rs b/src/docker/network.rs index b2ce2a9..a7c80fc 100644 --- a/src/docker/network.rs +++ b/src/docker/network.rs @@ -1,13 +1,12 @@ use bollard::query_parameters::ListNetworksOptionsBuilder; use bollard::secret::Network; use color_eyre::eyre::Result; -use serde::Serialize; use crate::docker::traits::DescribeSection; use super::traits::Describe; -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] pub struct DockerNetwork { pub id: String, pub name: String, diff --git a/src/docker/traits.rs b/src/docker/traits.rs index c8bdcfd..96bd918 100644 --- a/src/docker/traits.rs +++ b/src/docker/traits.rs @@ -34,7 +34,7 @@ pub struct DescribeItem { } impl DescribeItem { - pub(crate) fn new(name: N, value: V) -> Self { + pub fn new(name: N, value: V) -> Self { Self { id: Uuid::new_v4(), name: name.to_string(), @@ -44,7 +44,7 @@ impl DescribeItem { } impl DescribeSection { - pub(crate) fn new(name: N) -> Self { + pub fn new(name: N) -> Self { Self { id: Uuid::new_v4(), name: name.to_string(), @@ -52,12 +52,8 @@ impl DescribeSection { } } - pub(crate) fn push(&mut self, item: DescribeItem) -> &mut Self { - self.items.push(item); - self - } - pub(crate) fn item(&mut self, name: N, value: V) -> &mut Self { - self.push(DescribeItem::new(name, value)) + self.items.push(DescribeItem::new(name, value)); + self } } diff --git a/src/docker/volume.rs b/src/docker/volume.rs index c0a225b..97591ed 100644 --- a/src/docker/volume.rs +++ b/src/docker/volume.rs @@ -2,14 +2,13 @@ use bollard::query_parameters::{ListVolumesOptionsBuilder, RemoveVolumeOptionsBu use bollard::secret::{Volume, VolumeScopeEnum}; use byte_unit::{Byte, UnitType}; use color_eyre::eyre::{Result, bail}; -use serde::Serialize; use std::collections::HashMap; use crate::docker::traits::DescribeSection; use super::traits::Describe; -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] pub struct DockerVolume { pub name: String, pub driver: String, From 675d7a232e29e7a64169fc9ab239db9ab6514561 Mon Sep 17 00:00:00 2001 From: Vitali Pikulik Date: Wed, 7 Jan 2026 17:15:49 +0100 Subject: [PATCH 4/8] Improve --- src/docker/network.rs | 12 ++-------- src/docker/traits.rs | 12 ++++++++++ src/docker/volume.rs | 54 ++++++++++++++++++------------------------- src/pages/describe.rs | 4 ++-- 4 files changed, 38 insertions(+), 44 deletions(-) diff --git a/src/docker/network.rs b/src/docker/network.rs index a7c80fc..6277859 100644 --- a/src/docker/network.rs +++ b/src/docker/network.rs @@ -62,16 +62,8 @@ impl Describe for DockerNetwork { .item("Driver", &self.driver) .item("Created At", &self.created_at) .item("Scope", &self.scope) - .item( - "Internal", - self.internal.map(|v| v.to_string()).unwrap_or("N/A".into()), - ) - .item( - "Attachable", - self.attachable - .map(|v| v.to_string()) - .unwrap_or("N/A".into()), - ); + .item_opt("Internal", self.internal) + .item_opt("Attachable", self.attachable); Ok(vec![summary]) } } diff --git a/src/docker/traits.rs b/src/docker/traits.rs index 96bd918..789fc00 100644 --- a/src/docker/traits.rs +++ b/src/docker/traits.rs @@ -56,4 +56,16 @@ impl DescribeSection { self.items.push(DescribeItem::new(name, value)); self } + + pub(crate) fn item_opt( + &mut self, + name: N, + value: Option, + ) -> &mut Self { + self.items.push(DescribeItem::new( + name, + value.map(|v| v.to_string()).unwrap_or_default(), + )); + self + } } diff --git a/src/docker/volume.rs b/src/docker/volume.rs index 97591ed..d4436da 100644 --- a/src/docker/volume.rs +++ b/src/docker/volume.rs @@ -95,37 +95,27 @@ impl Describe for DockerVolume { .item("Name", &self.name) .item("Driver", &self.driver) .item("Mountpoint", &self.mountpoint) - .item("Created At", self.created_at.as_deref().unwrap_or("N/A")) - .item( - "Labels", - if self.labels.is_empty() { - "N/A".into() - } else { - format!("{}", self.labels.len()) - }, - ) - .item( - "Scope", - self.scope - .as_ref() - .map(|v| format!("{v}")) - .unwrap_or("N/A".into()), - ) - .item( - "Options", - if self.options.is_empty() { - "N/A".into() - } else { - format!("{}", self.options.len()) - }, - ) - .item( - "Reference Count", - self.ref_count - .map(|v| v.to_string()) - .unwrap_or("N/A".into()), - ) - .item("Size", self.size.as_deref().unwrap_or("N/A")); - Ok(vec![summary]) + .item_opt("Created At", self.created_at.as_ref()) + .item_opt("Scope", self.scope) + .item_opt("Reference Count", self.ref_count) + .item_opt("Size", self.size.as_ref()); + let mut result = vec![summary]; + + if !self.labels.is_empty() { + let mut label_section = DescribeSection::new("Labels"); + for (key, value) in &self.labels { + label_section.item(key, value); + } + result.push(label_section); + } + if !self.options.is_empty() { + let mut options_section = DescribeSection::new("Options"); + for (key, value) in &self.options { + options_section.item(key, value); + } + result.push(options_section); + } + + Ok(result) } } diff --git a/src/pages/describe.rs b/src/pages/describe.rs index e504167..b19a5cb 100644 --- a/src/pages/describe.rs +++ b/src/pages/describe.rs @@ -167,8 +167,8 @@ fn section_to_tree_item<'a>( .iter() .map(|item| { let line = Line::from(vec![ - Span::from(&item.name).style(key_style.clone()), - Span::from(":"), + Span::from(&item.name).style(*key_style), + Span::from(": "), Span::from(&item.value), ]); TreeItem::new_leaf(item.id, line) From cf0296845a6f23b2530f5fe6fe97796037828756 Mon Sep 17 00:00:00 2001 From: Vitali Pikulik Date: Wed, 7 Jan 2026 17:19:05 +0100 Subject: [PATCH 5/8] Remove unwrap --- src/pages/describe.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/describe.rs b/src/pages/describe.rs index b19a5cb..b056f6b 100644 --- a/src/pages/describe.rs +++ b/src/pages/describe.rs @@ -180,7 +180,7 @@ fn section_to_tree_item<'a>( Span::from(§ion.name).style(*section_style), items, ) - .unwrap(); + .expect("all items should be unique"); state.open(vec![section.id]); item } From 23242c4ab57f5d770e6c84604a9072f81fdb41a7 Mon Sep 17 00:00:00 2001 From: Vitali Pikulik Date: Mon, 26 Jan 2026 09:29:44 +0100 Subject: [PATCH 6/8] Cleanup --- src/pages/describe.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/pages/describe.rs b/src/pages/describe.rs index b056f6b..5fe8889 100644 --- a/src/pages/describe.rs +++ b/src/pages/describe.rs @@ -79,18 +79,6 @@ impl DescribeContainer { self.scroll -= 1; } } - - fn resolve_scroll(&mut self, height: &u16, n_lines: &u16) -> u16 { - let max_scroll = if *n_lines < (height / 2) { - 0 - } else { - n_lines - (height / 2) - }; - if self.scroll > max_scroll { - self.scroll = max_scroll; - }; - self.scroll - } } #[async_trait::async_trait] From 08dca7dcead7671402ed8eb5f3d7142cbbdf36ee Mon Sep 17 00:00:00 2001 From: Vitali Pikulik Date: Mon, 26 Jan 2026 09:33:54 +0100 Subject: [PATCH 7/8] Cleanup --- Cargo.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index eb8f244..ad70fe9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,9 +31,9 @@ lazy_static = "1.5.0" serde = "1.0.228" serde_yml = "0.0.12" tokio = { version = "1.49.0", features = [ - "rt-multi-thread", - "macros", - "process", + "rt-multi-thread", + "macros", + "process", ] } tracing = "0.1.44" tracing-error = "0.2.1" @@ -45,8 +45,8 @@ uuid = { version = "1.19.0", features = ["v4"] } ansi-to-tui = "8.0.1" ratatui-macros = "0.7.0" ratatui = { version = "0.30.0", features = [ - "serde", - "unstable-rendered-line-info", + "serde", + "unstable-rendered-line-info", ] } tui-big-text = "0.8.1" tui-tree-widget = "0.24.0" From e010a9a9d5c2de1cd22b2330d2c6388f731224e3 Mon Sep 17 00:00:00 2001 From: Vitali Pikulik Date: Fri, 20 Feb 2026 10:06:01 +0100 Subject: [PATCH 8/8] Fix scroll --- src/pages/describe.rs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/pages/describe.rs b/src/pages/describe.rs index 5fe8889..a7df877 100644 --- a/src/pages/describe.rs +++ b/src/pages/describe.rs @@ -40,7 +40,6 @@ pub struct DescribeContainer { tx: Sender>, cx: Option, page_help: Arc>, - scroll: u16, tree_state: TreeState, } @@ -56,7 +55,6 @@ impl DescribeContainer { tx, cx: None, page_help: Arc::new(Mutex::new(page_help)), - scroll: 0, tree_state: TreeState::default(), } } @@ -71,13 +69,11 @@ impl DescribeContainer { } fn down(&mut self) { - self.scroll += 1; + self.tree_state.scroll_down(1); } fn up(&mut self) { - if self.scroll > 0 { - self.scroll -= 1; - } + self.tree_state.scroll_up(1); } } @@ -188,21 +184,20 @@ impl Component for DescribeContainer { }) .collect_vec(); + let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(None) + .track_symbol(None) + .end_symbol(None); let widget = Tree::new(tree.as_slice()) .expect("all item identifiers are unique") - .experimental_scrollbar(Some( - Scrollbar::new(ScrollbarOrientation::VerticalRight) - .begin_symbol(None) - .track_symbol(None) - .end_symbol(None), - )) + .experimental_scrollbar(Some(scrollbar)) .highlight_style( Style::new() .fg(Color::Black) .bg(Color::LightGreen) .add_modifier(Modifier::BOLD), ) - .highlight_symbol(">> "); + .highlight_symbol(""); f.render_stateful_widget(widget, area, &mut self.tree_state); }