agentleFS
amir20amir20/dtop/CLAUDE.md

dtop

CLAUDE.md1.5k starsChanged 2 days agoMITView on GitHub

dtop is a terminal-based Docker container monitoring tool built with Rust. It provides real-time CPU, memory, network, and disk I/O metrics for Docker containers through a TUI interface, with support for both local and remote (SSH/TCP) Docker daemons. The tool supports monitoring multiple Docker hosts simultaneously and includes a built-in log viewer for streaming container logs. The docs site is a SvelteKit static site in the docs/ directory. Use bun (not npm) for all docs commands. The animated TUI on…

We split this CLAUDE.md into the rules it gives an agent

33 rules. About 0 are common, 18 are distinctive, and 15 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈18

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 15

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When releasing a new version, update the flake hashes:

    line 87
  2. Very specific:

    Update the version in flake.nix

    line 95
  3. Distinctive:

    Update all platform hashes automatically

    line 97
  4. Distinctive:

    Test the updated flake with:

    line 99
  5. Very specific:

    If config has all: true, the CLI flag cannot override it back to false

    line 201
  6. Very specific:

    All events include the host_id to identify their source

    line 351
  7. Very specific:

    Reconnects automatically: when the daemon goes away (e.g.

    line 352
  8. Distinctive:

    Stop/Restart use 10-second timeout before force kill

    line 392
  9. Distinctive:

    Remove uses force option to remove even if running

    line 393
  10. Very specific:

    All container managers share the same event channel (mpsc::Sender<AppEvent>)

    line 407
  11. Very specific:

    Every event includes a host_id to identify which host it came from

    line 408
  12. Very specific:

    Only works when not in an SSH session (detected via SSH_CLIENT/SSH_TTY/SSH_CONNECTION env vars)

    line 505
  13. Very specific:

    All fields except host are optional and use #[serde(skip_serializing_if = "Option::is_none")].

    line 582
  14. Distinctive:

    If CLI hosts are explicitly provided (not default), they override config file

    line 585
  15. Very specific:

    If CLI uses default (--host local) and config file has hosts, config file is used

    line 586
  16. Very specific:

    If both are empty/default, defaults to local

    line 587
  17. Very specific:

    Remove needs a second Enter (armed_bulk_action), and only runs if it still hits the same containers (armed_bulk_targets, compared as a set …

    line 624
  18. Very specific:

    per connection: If a remote lowers MaxSessions below 8, channel opens there will fail and the master gets evicted.

    line 668
  19. Very specific:

    If opening a channel fails (dead master after a network drop or daemon restart), that master is evicted and the attempt retries once on a …

    line 676
  20. Distinctive:

    Search bar at bottom when in SearchMode (filters containers as you type)

    line 756
  21. Distinctive:

    Default sort: Uptime (newest first, descending)

    line 779
  22. Distinctive:

    Search filtering is case-insensitive and filters by both name and ID

    line 792
  23. Very specific:

    Run cargo release <version> to create a new release

    line 920
  24. Very specific:

    Run tests with cargo test or cargo insta test for snapshot tests.

    line 1021
  25. Distinctive:

    Prefer extending an existing view (e.g.

    line 1034
  26. Distinctive:

    Stop at ~1,000 lines: If a PR's non-test, non-snapshot diff passes ~1,000 lines, stop and check scope with the user before continuing.

    line 1035
  27. Distinctive:

    Subagents and workflows get the out-of-scope list: Every implementation prompt includes "not in scope" and the rule: a deviation that adds …

    line 1040
  28. Distinctive:

    Audit size before review, not after: When a body of work is done, report its size per feature (code, tests, snapshots) next to its user …

    line 1050
  29. Distinctive:

    Before committing or submitting a pull request, always run:

    line 1058
  30. Distinctive:

    When reviewing pull requests for this repository, follow these guidelines:

    line 1066
  31. Distinctive:

    Skip minor nitpicks: - Don't comment on trivial style issues unless they significantly affect readability

    line 1074
  32. Distinctive:

    Be direct: - Use short, clear sentences.

    line 1075
  33. Distinctive:

    Group related feedback: - Don't create separate comments for each small issue

    line 1076

≈ “Common” is an estimate: at least 1 in 5 of the 25,680 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

dtop is a terminal-based Docker container monitoring tool built with Rust. It provides real-time CPU, memory, network, and disk I/O metrics for Docker containers through a TUI interface, with support for both local and remote (SSH/TCP) Docker daemons. The tool supports monitoring multiple Docker hosts simultaneously and includes a built-in log viewer for streaming container logs.

Build & Run Commands

# Development
cargo run                                    # Run with local Docker daemon (or config file)
cargo run -- --host ssh://user@host         # Run with remote Docker host via SSH
cargo run -- --host tcp://host:2375         # Run with remote Docker host via TCP
cargo run -- --host tls://host:2376         # Run with remote Docker host via TLS
cargo run -- --host local --host ssh://user@host1 --host tcp://host2:2375  # Multiple hosts
cargo run -- --filter status=running        # Filter to show only running containers
cargo run -- --filter name=nginx --filter label=env=prod  # Multiple filters
cargo run -- --all                           # Show all containers (including stopped/exited)
cargo run -- -a                              # Short version of --all
cargo run -- --sort name                     # Sort containers by name
cargo run -- -s cpu                          # Sort containers by CPU usage

# Self-update
cargo run -- update                          # Update dtop to the latest version
dtop update                                  # (or use the installed binary)

# Testing & Quality Checks
cargo fmt --check                            # Check formatting (must pass before committing)
cargo clippy                                 # Run linter (must pass before committing)
cargo test                                   # Run all tests
cargo test -- --nocapture                    # Run tests with output
cargo insta test                             # Run tests with snapshot review
cargo insta accept                           # Accept all pending snapshots
cargo insta reject                           # Reject all pending snapshots

# Production build
cargo build --release                        # The binary will be at target/release/dtop (includes self-update)
cargo build --release --no-default-features  # Build without self-update feature (smaller binary)

# Changelog generation
git-cliff --latest                           # Generate changelog for the latest tag
git-cliff --unreleased                       # Generate changelog for unreleased changes
git-cliff --tag v0.3.0..v0.3.6              # Generate changelog for a version range
git-cliff -o CHANGELOG.md                    # Write changelog to file

# Docker build
docker build -t dtop .
docker run -v /var/run/docker.sock:/var/run/docker.sock -it dtop

# Nix
nix run .                                    # Run using pre-built binary (fast)
nix run .#source                             # Run building from source
nix build                                    # Build the package
nix flake check                              # Verify flake is valid
nix develop                                  # Enter dev shell with Rust tooling

Documentation Site

The docs site is a SvelteKit static site in the docs/ directory. Use bun (not npm) for all docs commands.

cd docs
bun install                                  # Install dependencies
bun run dev                                  # Start dev server
bun run build                                # Production build
bun run preview                              # Preview production build

The animated TUI on the home page (docs/src/lib/components/TuiDemo.svelte) hand-ports dtop's rendering (column widths, header bar fitting, formatters, icons). Update it when changing src/ui/container_list.rs, src/ui/system_header.rs, src/ui/formatters.rs or the default columns.

Nix Flake

The project includes a Nix flake (flake.nix) for reproducible builds and easy installation.

Packages

  • packages.default - Pre-built binary from GitHub releases (instant install)
  • packages.source - Build from source using buildRustPackage

Updating Nix Hashes (for new releases)

When releasing a new version, update the flake hashes:

./scripts/update-nix-hashes.sh <VERSION>
# Example: ./scripts/update-nix-hashes.sh 0.6.8

This script requires Nix to be installed. It will:

  1. Update the version in flake.nix
  2. Fetch new release artifacts and compute their hashes
  3. Update all platform hashes automatically

Test the updated flake with:

nix build && ./result/bin/dtop --version

Configuration

The application supports configuration via YAML files. Config files are searched in the following order (first found wins):

  1. ./config.yaml or ./config.yml
  2. ./.dtop.yaml or ./.dtop.yml
  3. ~/.config/dtop/config.yaml or ~/.config/dtop/config.yml
  4. ~/.dtop.yaml or ~/.dtop.yml

--config <PATH> / -c <PATH> loads that file instead and skips the search. It must exist (a missing or invalid file is an error), it is loaded even when --host is given (--host still replaces its hosts), and Ctrl-S saves back to it.

Command line arguments take precedence over config file values.

Example config file (config.yaml):

hosts:
  - host: local
  - host: ssh://user@server1
    filter:
      - status=running
      - label=environment=production
  - host: tcp://192.168.1.100:2375
  - host: ssh://root@146.190.3.114
    dozzle: https://l.dozzle.dev/
    filter:
      - name=nginx
      - ancestor=ubuntu:24.04

# Icon style: "auto" (default), "unicode" or "nerd" (requires Nerd Font)
icons: auto

# OSC 8 hyperlinks on container names: "auto" (default), "always", or "never"
hyperlinks: auto

# System stats header above the container list (default: true; 't' toggles)
stats_header: true

# Show all containers (default: false, shows only running containers)
# Set to true to show all containers including stopped, exited, and paused containers
all: false

# Default sort field: "uptime" (default), "name", "cpu", or "memory"
sort: uptime

Each host entry is a struct with:

  • host: Docker connection string (required)
  • name: Optional display name shown in the header and Host column (defaults to a name derived from host, e.g. user@server1)
  • dozzle: Optional URL to Dozzle instance
  • filter: Optional list of Docker filters (e.g., ["status=running", "name=nginx"])
  • Future optional fields can be added as needed

Global config options:

  • icons: Icon style to use ("auto", "unicode" or "nerd"). auto (ui/icons.rs::IconMode::resolve) sends XTVERSION followed by DA1 right after raw mode is enabled and before the keyboard worker starts (which would otherwise eat the reply). DA1 is the sentinel: every terminal answers it, so a terminal that ignores XTVERSION costs one round trip, not the 1s timeout. Nerd icons on Ghostty, WezTerm and kitty >= 0.36, which bundle the glyphs; the query is in-band, so it works when dtop runs over SSH (where TERM_PROGRAM is lost and TERM is often rewritten). Falls back to TERM_PROGRAM/TERM when there is no reply. Under tmux the probe would name tmux, so it reads #{client_termtype} instead (tmux >= 3.3 runs XTVERSION against the outer terminal on attach); Unicode under screen. A reply later than the 1s timeout reaches the keyboard worker as keystrokes (only on links with a round trip over 1s); accepted rather than filtering input
  • all: Show all containers including stopped/exited (default: false)
  • sort: Default sort field for container list ("uptime", "name", "cpu", "memory")
  • sort_direction: Sort direction ("asc" or "desc") - if omitted, uses field's default
  • columns: List of column IDs to show, in order (e.g., ["status", "name", "cpu", "memory"])
  • hyperlinks: OSC 8 hyperlink mode for Dozzle links ("auto", "always", "never")
  • stats_header: Show the system stats header (default: true)

See config.example.yaml for a complete example.

Preferences Persistence

Preferences (columns, sort field, sort direction, show-all, stats header) can be saved to the config file:

  • Ctrl-S - Save current preferences to config file (creates ~/.config/dtop/config.yaml if no config exists)
  • Ctrl-R - Reset preferences to defaults (requires y/n confirmation)

Behavior:

  • Saving preserves other config keys (hosts, icons, filters)
  • Notifications show success/confirmation messages (auto-dismiss after 2 seconds)
  • Reset affects in-memory state only; saved config is not deleted

Show All Containers

The --all / -a flag controls whether to show all containers or only running containers:

  • CLI: --all or -a flag (boolean, follows docker ps -a convention)
  • Config file: all: true/false in YAML

Behavior:

  • By default, only running containers are shown
  • The --all flag enables showing all containers (including stopped, exited, paused)
  • The flag is one-way enable only: it can enable showing all containers but cannot disable it
  • If config has all: true, the CLI flag cannot override it back to false
  • Users can always toggle the view with the 'a' key in the UI

Examples:

dtop --all              # Show all containers
dtop -a                 # Short version
dtop                    # Show running only (unless config has all: true)

Note: This design matches Docker's docker ps -a behavior where the flag is a simple boolean enable.

Default Sort Field

The --sort / -s option sets the default sort field for the container list:

  • CLI: --sort <field> or -s <field>
  • Config file: sort: <field> in YAML

Available sort fields:

  • uptime (or u) - Sort by container creation time (default, newest first)
  • name (or n) - Sort by container name (alphabetically, ascending)
  • cpu (or c) - Sort by CPU usage (highest first)
  • memory (or m) - Sort by memory usage (highest first)

Behavior:

  • Each field has a default sort direction (uptime/cpu/memory: descending, name: ascending)
  • CLI takes precedence over config file
  • Users can change the sort field and toggle direction in the UI with 's' (opens sort selector popup)

Examples:

dtop --sort name        # Sort by name alphabetically
dtop -s cpu             # Sort by CPU usage (highest first)
dtop --sort memory      # Sort by memory usage

Container Filtering

The application supports Docker filters similar to docker ps --filter. Filters can be specified via:

  • CLI: --filter or -f flag (applies to all hosts)
  • Config file: Per-host filter field in YAML (host-specific filters)

CLI filters take precedence over config file filters.

Available filters (container listing):

  • id - Container ID (full or partial)
  • name - Container name (supports partial matches)
  • label - Label key or key=value pair
  • status - Container state: created, restarting, running, removing, paused, exited, dead
  • ancestor - Image name/tag or descendant
  • before - Created before container ID/name
  • since - Created after container ID/name
  • volume - Mounted volume or path
  • network - Connected network
  • publish/expose - Published/exposed ports
  • health - Healthcheck status: starting, healthy, unhealthy, none
  • exited - Exit code
  • isolation - Isolation type (Windows only)
  • is-task - Service task containers (boolean)

Filter Logic:

  • Multiple values for same filter = OR logic: --filter status=running --filter status=paused
  • Different filter types = AND logic: --filter status=running --filter name=nginx

Events API Compatibility: Some filters only work with container listing, not the events stream. The application will log warnings for incompatible filters (e.g., status, ancestor, health) that won't apply to real-time event monitoring. Compatible filters for both listing and events include: label, network, volume.

Filters id and name are automatically mapped to the container filter for events API compatibility.

Architecture

The application follows an event-driven architecture with multiple async/threaded components communicating via a single mpsc channel (AppEvent). The architecture supports multi-host monitoring by spawning independent container managers for each Docker host.

Source Code Organization

The codebase is organized into logical modules:

src/
├── cli/                   # CLI-related modules
│   ├── config.rs         # Configuration file loading (YAML)
│   ├── connect.rs        # Docker host connection and verification
│   ├── filters.rs        # Docker filter parsing (--filter support)
│   └── update.rs         # Self-update functionality
│
├── core/                  # Core application logic
│   ├── app_state/        # Central state manager (modularized)
│   │   ├── mod.rs        # AppState struct and main event dispatcher
│   │   ├── actions.rs    # Action menu handling (start/stop/restart/remove)
│   │   ├── container_events.rs  # Container lifecycle event handlers
│   │   ├── integrations.rs      # Dozzle integration handlers
│   │   ├── log_view.rs   # Log view event handlers
│   │   ├── navigation.rs # Selection and navigation handlers
│   │   ├── search.rs     # Search mode and filtering handlers
│   │   └── sorting.rs    # Container sorting logic
│   └── types.rs          # Core types and events
│
├── docker/                # Docker-related functionality
│   ├── connection.rs     # Container manager & Docker host abstraction
│   ├── details.rs        # Inspect for the details popup
│   ├── logs.rs           # Log streaming
│   ├── stats.rs          # Stats streaming and calculation
│   └── actions.rs        # Container actions (start/stop/restart/remove)
│
├── ui/                    # UI rendering and input handling
│   ├── input.rs          # Keyboard worker
│   ├── render.rs         # Ratatui UI rendering
│   ├── container_list.rs # Container list table rendering
│   ├── action_menu.rs    # Action menu popup rendering
│   ├── details.rs        # Details popup rendering
│   ├── help.rs           # Help popup rendering
│   ├── icons.rs          # Icon sets (Unicode and Nerd Font)
│   └── ui_tests.rs       # UI snapshot tests
│
├── lib.rs                # Library root with module declarations
└── main.rs               # Binary entry point

Core Components

  1. Main Event Loop (main.rs::run_event_loop)
    • Receives events from all container managers via a shared channel
    • Delegates state management to AppState struct
    • Renders UI at 500ms intervals using Ratatui
    • Uses throttling to wait for events or timeout, then drains all pending events
  2. AppState (core/app_state/mod.rs::AppState)
    • Central state manager that handles all runtime data
    • Maintains container state in HashMap<ContainerKey, Container> where ContainerKey is (host_id, container_id)
    • Manages view state (container list, log view, action menu, search mode)
    • Handles log streaming, scrolling, and auto-scroll behavior
    • Pre-sorts containers by host_id and selected sort field for efficient rendering
    • Single source of truth for container data across all hosts
    • Modular architecture: Event handlers are split across specialized modules:
      • actions.rs: Action menu navigation and execution
      • container_events.rs: Container lifecycle and stats updates
      • integrations.rs: Dozzle integration
      • log_view.rs: Log streaming and scrolling
      • navigation.rs: Selection and navigation
      • search.rs: Search filtering and input handling
      • sorting.rs: Container sorting by multiple fields
  3. Container Manager (docker/connection.rs::container_manager) - One per Docker host
    • Async task that manages Docker API interactions for a specific host
    • Each manager operates independently with its own DockerHost instance
    • Fetches initial container list on startup
    • Subscribes to Docker events (start/stop/die) for that host
    • Spawns individual stats stream tasks per container
    • Each container gets its own async task running stream_container_stats
    • All events include the host_id to identify their source
    • Reconnects automatically when the daemon goes away (e.g. a docker package upgrade restarts it and drops the socket). When the event stream breaks, the per-container stats tasks are aborted, HostDisconnected is emitted so the UI shows a "reconnecting" banner, and the host is pinged with a capped backoff (1s → 5s) until it answers. On success HostReconnected is emitted and the container list is re-fetched, which re-synchronizes state for that host.
  4. Stats Streaming (docker/stats.rs::stream_container_stats)
    • One async task per container that streams real-time stats
    • Uses exponential moving average (alpha=0.3) to smooth CPU, memory, network, and disk I/O stats
    • Calculates network TX/RX rates in bytes per second
    • Calculates disk read/write rates in bytes per second from blkio_stats.io_service_bytes_recursive
    • CPU calculation: Delta between current and previous usage, normalized by system CPU delta and CPU count
    • Memory calculation: Current usage divided by limit, expressed as percentage
    • Note: Disk I/O stats may return zeros on some systems with certain cgroup configurations (see moby/moby#35352)
    • Re-opens a broken stream with a capped backoff (1s → 5s) as long as the container is still running, which is checked with an inspect before each retry so a stopped or removed container does not spin against a 404. Stream errors are logged; swallowing them left a container's row at zero for every metric for the rest of the session, because nothing re-armed monitoring except a fresh start event. For the same reason container_manager treats a finished stats task as unmonitored — its handle stays in active_containers until a stop/die/destroy event prunes it.
  5. Log Streaming (docker/logs.rs::stream_container_logs)
    • Streams logs from a container in real-time
    • Fetches last 100 lines on startup, then follows new logs
    • Parses timestamps (RFC3339 format) and messages
    • Uses ansi-to-tui to parse ANSI escape codes for colored output
    • Preserves whitespace and formatting from original logs
    • Sends each log line as AppEvent::LogLine event with pre-parsed Text
  6. Keyboard Worker (ui/input.rs::keyboard_worker)
    • Blocking thread that polls keyboard input every 200ms
    • Handles: 'q'/Ctrl-C (quit), Enter (show action menu/execute), Esc (exit view/cancel), Up/Down (navigate/scroll)
    • Arrow keys: Right (view logs), Left (exit log view), '/' (search mode)
    • Separate thread because crossterm's event polling is blocking
  7. Container Actions (docker/actions.rs::execute_container_action)
    • Async execution of Docker container actions
    • Supports: Start, Stop, Restart, Remove
    • Stop/Restart use 10-second timeout before force kill
    • Remove uses force option to remove even if running
    • Sends progress events (InProgress, Success, Error) back to main event loop

Multi-Host Architecture

Host1 (local)     → container_manager → AppEvent(host_id="local", ...) ┐
Host2 (server1)   → container_manager → AppEvent(host_id="server1", ...)├→ Main Loop → UI
Host3 (server2)   → container_manager → AppEvent(host_id="server2", ...)┘
Keyboard          → keyboard_worker   → AppEvent::Quit → Main Loop → Exit

Key Design Points:

  • Each host runs its own independent container_manager task
  • All container managers share the same event channel (mpsc::Sender<AppEvent>)
  • Every event includes a host_id to identify which host it came from
  • Containers are uniquely identified by ContainerKey { host_id, container_id }
  • The UI displays host information alongside container information

Event Types (core/types.rs::AppEvent)

Container-related events use structured types to identify containers across hosts:

  • InitialContainerList(HostId, Vec<Container>) - The authoritative container list for a host, sent on startup and again whenever the host reconnects (containers held for that host but missing from the list are dropped)
  • ContainerCreated(Container) - New container started (host_id is in the Container struct)
  • ContainerDestroyed(ContainerKey) - Container stopped/died (identified by host_id + container_id)
  • ContainerStat(ContainerKey, ContainerStats) - Stats update (identified by host_id + container_id)
  • ContainerHealthChanged(ContainerKey, HealthStatus) - Health status changed for a container
  • ContainerDetails(ContainerKey, Result<ContainerDetails, String>) - Inspect result for the details popup
  • Quit - User pressed 'q' or Ctrl-C
  • Resize - Terminal was resized
  • SelectPrevious - Move selection up (Up arrow in container list)
  • SelectNext - Move selection down (Down arrow in container list)
  • EnterPressed - User pressed Enter to show action menu or execute action
  • ExitLogView - User pressed Left arrow to exit log view
  • ShowLogView - User pressed Right arrow to view logs
  • ScrollUp - Scroll up in log view (Up arrow)
  • ScrollDown - Scroll down in log view (Down arrow)
  • LogLine(ContainerKey, LogEntry) - New log line received from streaming logs
  • OpenDozzle - User pressed 'o' to open Dozzle for selected container
  • ToggleHelp - User pressed '?' to toggle help popup
  • KeyInput(KeyEvent) - User pressed 's' to open sort selector popup
  • ToggleShowAll - User pressed 'a' to toggle showing all containers (including stopped)
  • CancelActionMenu - User pressed Esc to cancel action menu or exit views
  • SelectActionUp - Navigate up in action menu (Up arrow)
  • SelectActionDown - Navigate down in action menu (Down arrow)
  • ActionInProgress(ContainerKey, ContainerAction) - Container action started
  • ActionSuccess(ContainerKey, ContainerAction) - Container action completed successfully
  • ActionError(ContainerKey, ContainerAction, String) - Container action failed
  • EnterSearchMode - User pressed '/' to enter search mode
  • SearchKeyEvent(KeyEvent) - Key event for search input (passed to tui-input)
  • HostDisconnected(HostId) - Lost the connection to a previously connected host; it is being retried in the background
  • HostReconnected(HostId) - A disconnected host is reachable again
  • HostInfo(HostId, HostInfo) - CPU count and total memory from docker info, sent on every (re)connect
  • SystemStats(SystemStats) - A sample of the local machine's CPU/memory/swap/network/load/uptime (every 2s)
  • RemoteSystemStats(HostId, SystemStats) - The same for an SSH host, read from its /proc (every 2s)

View States (core/types.rs::ViewState)

The application has these view states:

  • ContainerList - Main view showing all containers across all hosts
  • LogView(ContainerKey) - Log viewer for a specific container with real-time streaming
  • ActionMenu(ContainerKey) - Action menu popup for a specific container
  • BulkActionMenu - Action menu popup for every marked container
  • Details(ContainerKey) - Details popup for a container, opened from its action menu
  • SearchMode - Search mode for filtering containers by name/ID
  • ColumnSelector - Column visibility and ordering popup
  • SortSelector - Sort field selection popup

Container Data Model (core/types.rs::Container)

The Container struct holds both static metadata and runtime statistics:

pub struct Container {
    pub id: String,                         // Truncated container ID (12 chars)
    pub name: String,                       // Container name
    pub state: ContainerState,              // Running, Paused, Exited, etc.
    pub health: Option<HealthStatus>,       // Healthy, Unhealthy, Starting (None if no health check)
    pub created: Option<DateTime<Utc>>,     // Container creation timestamp
    pub stats: ContainerStats,              // CPU, memory, network, disk I/O stats (updated in real-time)
    pub host_id: HostId,                    // Which Docker host this container belongs to
    pub dozzle_url: Option<String>,         // Dozzle URL for this container's host
}

Container Identification:

  • Containers are uniquely identified by ContainerKey { host_id, container_id }
  • This allows tracking the same container ID across different hosts
  • Container IDs are truncated to 12 characters (Docker API accepts partial IDs)

Docker Host Abstraction

The DockerHost struct (docker/connection.rs) encapsulates a Docker connection with its identifier and optional Dozzle URL:

pub struct DockerHost {
    pub host_id: HostId,
    pub docker: Docker,
    pub dozzle_url: Option<String>,
}

Host IDs are derived from the host specification:

  • "local" → host_id = "local"
  • "ssh://user@host" → host_id = "user@host"
  • "ssh://user@host:2222" → host_id = "user@host" (port stripped)

Dozzle Integration:

  • Dozzle URLs can be configured per-host in the config file
  • Press 'o' in container list view to open Dozzle for the selected container
  • Opens in browser at {dozzle_url}/container/{container_id} format
  • Only works when not in an SSH session (detected via SSH_CLIENT/SSH_TTY/SSH_CONNECTION env vars)
  • Container names are also rendered as clickable [OSC 8] hyperlinks to the same URL (see OSC 8 Hyperlinks below). Unlike the 'o' key, these do work over SSH, because the link is resolved by the user's local terminal emulator.

OSC 8 Hyperlinks

The hyperlinks config key / --hyperlinks flag controls whether container names in the Name column render as clickable hyperlinks pointing at Dozzle. Rows whose host has no dozzle URL are left as plain text.

  • auto (default) — enable only on terminals detected as supporting OSC 8 (src/ui/hyperlinks.rs::detect_osc8_support). Detection sniffs TERM_PROGRAM, TERM, WT_SESSION, ALACRITTY_WINDOW_ID, KONSOLE_VERSION, and VTE_VERSION. Multiplexers (TMUX, TERM=screen*/tmux*) are off under auto: tmux >= 3.4 needs set -ga terminal-features "*:hyperlinks" and that can't be probed without shelling out.
  • always — render regardless of terminal. The escape hatch for tmux users who have configured passthrough.
  • never — never render.

Implementation note: Table builds its cells from Text, which has nowhere to carry an escape sequence, so the links can't be nested inside the table. render_dozzle_links (src/ui/container_list.rs) draws Hyperlink widgets (src/ui/hyperlinks.rs) on top of the already-rendered table. To find the Name column's rect it replays the same Block::inner and Layout::horizontal calls the table used, rather than hardcoding offsets, so the overlay can't drift from the table's own layout. The row offset is read from TableState::offset() after the table renders, since Table::render_rows is what writes the first visible index back into the state. Labels are pre-clipped with clip_to_width so a linked name truncates the same way an unlinked one does.

CellDiffOption::ForcedWidth is load-bearing. Buffer::diff decides how many terminal columns a cell occupies from its symbol's display width, and an OSC 8 escape sequence measures dozens of columns wide while printing nothing. A cell holding an escape sequence must therefore declare CellDiffOption::ForcedWidth(<real width>), or the diff advances past that bogus width and silently drops every following cell on the row — in practice the CPU and Memory columns simply stop rendering.

For that reason Hyperlink writes the label as ordinary per-cell text and puts escape sequences only on the first and last cells (opener prepended to the first grapheme, terminator appended to the last), each tagged ForcedWidth. This also keeps the row diffing, snapshotting, and copy-pasting as its plain text, and keeps the backend's cursor-position tracking in sync (each cell still advances the real cursor by exactly its declared width).

URL safety: the Dozzle URL is written verbatim inside the escape sequence, so Hyperlink refuses to render a link whose URL contains control characters (is_safe_url) — otherwise a dozzle: value from a shared or synced config file could close the sequence early and inject arbitrary escapes into the terminal. It fails closed (plain text, no link) rather than stripping, since rewriting a URL could point the link somewhere the user never configured. Labels are safe by construction: control characters are zero-width and get dropped during placement.

This is why the third-party hyperrat crate is not used: as of 0.1.3 it packs the entire sequence plus the label into a single cell with CellDiffOption::None, which triggers exactly the row-swallowing bug above. test_hyperlink_does_not_swallow_following_columns and test_hyperlink_reaches_the_real_backend_without_eating_the_row (ui/ui_tests.rs) guard against regressing this; note that the second is the only test that exercises the diff → real-backend byte path, where this class of bug is actually visible.

Configuration Loading

The Config struct (cli/config.rs) handles YAML configuration file loading:

  • Searches multiple locations in priority order (see Configuration section above)
  • Merges config file values with CLI arguments (CLI takes precedence)
  • Uses serde_norway (maintained serde_yaml fork) for deserialization
  • Uses dirs crate for home directory detection

Host Configuration Format: The HostConfig struct contains:

  • host: String - The Docker connection string (required)
  • dozzle: Option<String> - URL to Dozzle instance (optional)
  • Additional optional fields can be added in the future

All fields except host are optional and use #[serde(skip_serializing_if = "Option::is_none")].

The merge logic:

  • If CLI hosts are explicitly provided (not default), they override config file
  • If CLI uses default (--host local) and config file has hosts, config file is used
  • If both are empty/default, defaults to local
  • CLI hosts are converted to HostConfig structs with dozzle: None

Container Actions System

The application supports interactive container management through an action menu (ui/action_menu.rs and docker/actions.rs):

Action Flow:

  1. User presses Right arrow (→) in container list
  2. Action menu popup appears with available actions based on container state
  3. User selects action with Up/Down arrows and presses Enter
  4. Action is executed asynchronously via execute_container_action()
  5. Docker events automatically update container state in UI

Available Actions:

  • Start: Available for Exited, Created, Dead containers
  • Stop: Available for Running, Paused containers (10-second timeout)
  • Restart: Available for Running containers (10-second timeout)
  • Remove: Available for any state except Restarting/Removing (forced removal)
  • Details: Available in every state (listed last, never in the bulk menu). Opens a popup filled by one inspect (docker/details.rs): image, command, created/started, last exit code and OOM kill, restart policy and count, ports, mounts, networks, label count. Environment variables are deliberately left out, since they often hold secrets

State-Based Availability:

  • Running → Shell, Stop, Restart, Remove, Details
  • Paused → Stop, Remove, Details
  • Exited/Created/Dead → Start, Remove, Details
  • Restarting/Removing → Details only

Bulk Actions (core/app_state/selection.rs):

  • Marks live in AppState::marked_containers; they are dropped with their container. Running an action unmarks only the containers it ran on; the title counts hidden marks
  • Bulk actions act only on marked containers that are listed (marked_visible_keys), so a container hidden by search or the show-all toggle is never touched unseen
  • The bulk menu offers Start/Stop/Restart/Remove if any marked container supports them (no Shell), each labelled "(n of m)"; an action skips containers it doesn't suit
  • Remove needs a second Enter (armed_bulk_action), and only runs if it still hits the same containers (armed_bulk_targets, compared as a set since the list re-sorts). When container changes reshape an open menu, revalidate_action_menu keeps the cursor on the same action, or disarms if it is gone
  • Failed containers are re-marked, so retrying them is one Enter away
  • Results are tracked per batch in AppState::bulk_progress (batches can overlap) and reported as one summary toast: "Stopped 4 containers", or "Stopped 2 of 4 containers (2 failed, last: <reason>)". A one-container batch leaves its own error toast up instead. Containers on a disconnected host are skipped (and stay marked); a host disconnect drops batches waiting on it, and batches with no event for 60s are dropped (measured from the last event, since a large batch run 4 at a time can rightly take minutes). A container launched in a new batch is removed from any older batch
  • Each host runs at most 4 of a batch's actions at once (BULK_CONCURRENCY_PER_HOST), so marking dozens of containers doesn't burst an SSH host's shared masters
  • An action menu left with no actions (its containers went away) closes itself (revalidate_action_menu)

Implementation Details:

  • Actions spawn async tasks that don't block the UI
  • Progress events sent back to main loop (InProgress, Success, Error)
  • Container state updates happen automatically via Docker event stream
  • Action menu closes immediately after execution for responsive UX

Docker Connection

The connect_docker() function (docker/connection.rs) handles these connection modes:

  • --host local: Resolves the endpoint via the Docker CLI's rules (see below), falling back to the OS default socket
  • --host unix:///path/to/docker.sock: Connects to a specific Unix socket
  • --host ssh://user@host[:port]: Connects via SSH (requires Bollard SSH feature)
  • --host tcp://host:port: Connects via TCP to remote Docker daemon (unencrypted)
  • --host tls://host:port: Connects via TLS to remote Docker daemon (encrypted, requires DOCKER_CERT_PATH)

Multiple --host arguments can be provided to monitor multiple Docker hosts simultaneously.

SSH Transport (docker/ssh.rs): dtop uses its own SSH connector rather than Docker::connect_with_ssh, because bollard drops the ssh:// scheme before resolving the destination, which breaks custom ports.

dtop holds one long-lived HTTP connection per container (the stats stream), plus the event stream and any open log stream. To avoid one TCP connection and auth handshake per container, connections are multiplexed over a pool of shared SSH control masters, each carrying up to 8 channels (MAX_CHANNELS_PER_MASTER). 30 containers use 4 connections instead of 31.

  • The cap of 8 stays under sshd's default MaxSessions of 10, which limits channels per connection. If a remote lowers MaxSessions below 8, channel opens there will fail and the master gets evicted.
  • Master creation is serialized (the pool lock is held across the launch) so a startup burst doesn't open many simultaneous handshakes — relevant to sshd's MaxStartups, which throttles concurrent unauthenticated connections, and to fail2ban-style tooling that reads a connection burst as an attack.
  • A channel slot is tied to the connection's lifetime via a semaphore permit held in SshStream; the slot returns to the pool when the connection closes.
  • If opening a channel fails (dead master after a network drop or daemon restart), that master is evicted and the attempt retries once on a fresh one. This is what lets the reconnect loop in connection.rs heal an SSH host.
  • Masters are launched with ServerAliveInterval=15s so a dead master surfaces as an error instead of hanging. Dropping the last reference runs ssh -O exit, so masters are cleaned up on quit.
  • SshRunner (returned by connect_with_ssh, stored on DockerHost::ssh) runs short commands on the host over the same pool, borrowing one channel per command. It is what the remote system stats sampler uses. Usually that's a free slot on an existing master, but if every master is exactly full the sample launches one more (one extra connection over the counts above, relevant to MaxStartups/fail2ban). A timed-out command is dropped, and openssh's kill_on_drop closes its channel.

Docker Context Resolution (docker/context.rs): For the local host, dtop mirrors the Docker CLI's endpoint resolution so it works out of the box with colima, Rancher Desktop, and similar tools. Resolution order:

  1. DOCKER_HOST environment variable (any scheme)
  2. DOCKER_CONTEXT environment variable naming a context
  3. currentContext in ~/.docker/config.json
  4. The default context (OS default socket)

For a named context, the endpoint is read from ~/.docker/contexts/meta/<sha256(name)>/meta.json. The local host_id is preserved in the UI regardless of the resolved endpoint.

Note: TCP connections are unencrypted. Only use on trusted networks or with proper firewall rules. For encrypted connections, use TLS with certificates.

Stats Calculation

Stats are calculated in docker/stats.rs with exponential smoothing applied:

  • CPU: Delta between current and previous CPU usage, normalized by system CPU delta and CPU count
  • Memory: Current usage divided by limit, expressed as percentage
  • Network: Calculates TX/RX rates by tracking byte deltas over time
  • Disk I/O: Calculates read/write rates from blkio_stats.io_service_bytes_recursive, aggregating "Read" and "Write" operations across all devices
  • Smoothing: Uses exponential moving average with alpha=0.3 to reduce noise and create smoother visualizations

UI Rendering

The UI (ui/render.rs) uses pre-allocated styles to avoid per-frame allocations.

Four View Modes:

  1. Container List View - Main table showing all containers
    • System stats header (ui/system_header.rs) between the title and the table. Toggled with t, or stats_header: false in config (saved by Ctrl-S). Lines, in order:
      • System line for the machine dtop runs on, sampled with sysinfo by core/system_stats.rs::system_stats_worker on its own thread (sysinfo refreshes block). Only spawned for the local host; an explicit unix:// socket may be forwarded from another machine, so it doesn't get one.
      • Then per host, local first: a System line for ssh:// hosts and a Docker line.
      • Remote System lines come from docker/remote_stats.rs, which runs one cat /proc/... script every 2s over the host's existing SSH masters (via SshRunner, see SSH Transport). CPU and network rates are deltas between two reads. A host is dropped only after 3 reads in a row find no /proc (not Linux); every other failure (connection, killed cat, bad output) is retried forever with a 10s → 60s backoff. run_sh times out after 10s and never evicts a master (a dead one is replaced by the Docker reconnect). Cleared on HostDisconnected.
      • Docker lines: CPU and memory summed over the host's running containers (AppState::docker_totals), measured against NCPU/MemTotal from docker info (container CPU is per-core, so the sum is divided by the core count). Under Docker Desktop/colima those describe the VM, not the Mac.
      • Network is on System lines only, and skips loopback, veth*, docker* and br-* (is_virtual_interface): bridges and veths re-count container traffic. Docker lines have none because summed container counters include traffic between containers (counted twice, as TX and RX) that never reaches the machine's interfaces, so it could exceed the machine's own traffic.
      • Rendering formats each line once into one buffer and measures widths from field ranges (HeaderText); keep it that way, render_stats_header_allocations guards the per-frame allocation cost. Both samplers idle while the header is hidden (AppState::header_visible, set via set_show_system_header).
      • The CPU/MEM bars are sized so the widest line fills the terminal width (same width on every line, so they align). When space runs short, detail is shed in order: uptime, load, swap, bars, core count.
      • Rows are reserved by growing the list block's top padding, so block.inner() (and the hyperlink overlay) stay correct. Hidden on terminals too short to fit it plus 8 list rows.
    • Dynamically shows/hides "Host" column (only shown when multiple hosts are connected)
    • Displays: ID, Name, Host (conditional), CPU%, Memory%, PIDs, Net TX, Net RX, Disk R, Disk W, Uptime, Status
    • PIDs and Disk I/O columns (Disk R, Disk W) are hidden by default - enable via column selector ('c')
    • PIDs column shows the current number of processes/threads; when a PID limit is set it displays as current/limit
    • Progress bars with percentage indicators for CPU and Memory
    • Network and disk rates formatted as B/s, KB/s, MB/s, or GB/s
    • Search bar at bottom when in SearchMode (filters containers as you type)
  2. Log View - Full-screen log streaming for selected container
    • Shows last 100 lines initially, then follows new logs
    • Timestamps displayed in yellow with bold formatting
    • Auto-scroll when at bottom, manual scroll preserves position
    • Displays "[AUTO]" or "[MANUAL]" indicator in title
  3. Action Menu - Centered popup for container actions
    • Shows available actions based on container state
    • Actions: Start (stopped), Stop (running), Restart (running), Remove (any state)
    • Displays container name and host in title
    • Visual feedback with icons (▶ Start, ■ Stop, ↻ Restart, 🗑 Remove)
  4. Search Mode - Filter containers by name/ID
    • Shows search input at bottom of container list
    • Filters containers in real-time as you type
    • Uses tui-input widget for text editing
    • Escape or Enter to exit search mode

Color Coding for Metrics:

  • Green: 0-50%
  • Yellow: 50.1-80%
  • Red: >80%

Sorting: Containers can be sorted by multiple fields:

  • Default sort: Uptime (newest first, descending)
  • Sort fields: Uptime, Name, CPU, Memory, PIDs, Net TX, Net RX, Disk Read, Disk Write, ID, Host, Status, Restarts
  • Containers are sorted globally across all hosts by the selected field, with host_id as tiebreaker
  • Press 's' to open sort selector popup
  • In the sort popup, select a field to sort by; selecting the active field toggles direction
  • Each field has a default direction: Name/ID/Host (ascending), Uptime/CPU/Memory/Network/Disk (descending)
  • Sort state is tracked in SortState with field and direction

Container Filtering:

  • By default, only running containers are shown
  • Press 'a' to toggle showing all containers (including stopped/exited containers)
  • Filter state is tracked in AppState::show_all_containers
  • Press '/' to enter search mode and filter by container name/ID
  • Search filtering is case-insensitive and filters by both name and ID
  • Filtered containers are automatically re-sorted after filtering

Health Status:

  • Containers with health checks display their status: Healthy, Unhealthy, Starting
  • Health status is parsed from Docker's health check information
  • Status changes trigger UI updates via ContainerHealthChanged event

CI/CD Workflows

The project uses cargo-dist for building and releasing binaries across multiple platforms.

Release Workflow (.github/workflows/release.yml)

  • Triggers on version tags (e.g., v0.1.0)
  • Uses cargo-dist for cross-platform builds
  • Builds for multiple platforms: Linux (x86_64, ARM64), macOS (x86_64, ARM64)
  • Automatically creates GitHub releases with generated changelogs
  • Produces installers, archives, and checksums
  • Three main jobs:
    • plan: Determines what needs to be built
    • build-local-artifacts: Builds platform-specific binaries
    • build-global-artifacts: Creates installers and checksums
    • host: Uploads artifacts and creates GitHub release

Other Workflows

  • .github/workflows/pr-build.yml - Builds on pull requests
  • .github/workflows/docker-build.yml - Builds Docker images for testing
  • .github/workflows/docker-release.yml - Publishes Docker images on release
  • .github/workflows/test.yml - Runs test suite

Build Features

The project supports optional features to control binary size and dependencies:

self-update Feature (enabled by default)

  • Adds the dtop update subcommand for self-updating the binary
  • Depends on self_update crate with rustls (adds ~1.9MB to binary size)
  • Included in: Release binaries, cargo-dist builds, regular cargo builds
  • Excluded from: Docker images (to minimize image size)

Usage:

# Build with self-update (default)
cargo build --release                        # Binary: ~3.8MB

# Build without self-update (smaller)
cargo build --release --no-default-features  # Binary: ~1.9MB

Docker Configuration: The Dockerfile builds with --no-default-features to create minimal Docker images (~2.5MB vs ~4.7MB). Since Docker containers are typically updated by pulling new images, the self-update feature isn't needed.

Changelog Management

The project uses git-cliff for automated changelog generation based on conventional commits.

Configuration

The changelog is configured in cliff.toml with the following settings:

  • Follows conventional commit format (feat, fix, docs, chore, etc.)
  • Groups commits by type (Features, Bug Fixes, Documentation, etc.)
  • Filters out dependency update commits and release preparation commits
  • Supports semantic versioning tags (v[0-9]+.[0-9]+.[0-9]+)
  • Sorts commits within sections by oldest first

Conventional Commit Format

Commits should follow this format:

<type>(<scope>): <description>

[optional body]

[optional footer]

Common types:

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation changes
  • perf: Performance improvements
  • refactor: Code refactoring
  • style: Code style changes
  • test: Test changes
  • chore: Maintenance tasks (builds, CI, etc.)

Examples:

feat(update): add self-update command
fix(stats): correct CPU calculation for multi-core systems
docs: update installation instructions
chore(deps): update rust crate bollard to v0.19.4

Generating Changelogs

# View latest release changelog
git-cliff --latest

# View unreleased changes
git-cliff --unreleased

# Generate changelog for a version range
git-cliff --tag v0.3.0..v0.3.6

# Write full changelog to file
git-cliff -o CHANGELOG.md

# Generate changelog and update for next version
git-cliff --unreleased --tag v0.4.0 -o CHANGELOG.md

Integration with Cargo

The project integrates git-cliff with both cargo-release and cargo-dist:

cargo-dist integration (dist-workspace.toml):

  • generate-changelog = true - Automatically generates changelogs during releases
  • changelog-backend = "git-cliff" - Uses git-cliff for changelog generation
  • The GitHub release workflow will automatically include the generated changelog

cargo-release integration (Cargo.toml):

  • pre-release-replacements - Automatically updates CHANGELOG.md during cargo release
  • Adds new version entry when creating a release

Release Workflow:

  1. Make changes and commit using conventional commit format
  2. Run cargo release <version> to create a new release
  3. CHANGELOG.md is automatically updated with the new version
  4. Tag is created and pushed to GitHub
  5. GitHub Actions (via cargo-dist) builds binaries and creates a release with the changelog

The CHANGELOG.md file is automatically maintained and should be committed to the repository.

Key Dependencies

  • Tokio: Async runtime for Docker API and event handling
  • Bollard: Docker API client with SSH support (requires ssh feature)
  • Ratatui: Terminal UI framework (v0.29)
  • Crossterm: Cross-platform terminal manipulation (v0.29)
  • Clap: CLI argument parsing with derive macros
  • Serde/Serde_yaml: Configuration file deserialization
  • Dirs: Cross-platform home directory detection
  • Chrono: Date and time handling for log timestamps
  • Futures-util: Stream utilities for async operations
  • Open: Cross-platform URL opener for Dozzle integration
  • Ansi-to-tui: ANSI escape code parsing for colored log output
  • Timeago: Human-readable time formatting for container uptime
  • Tui-input: Text input widget for search functionality
  • Unicode-width: Display-width calculation for placing/clipping hyperlink labels
  • Sysinfo: Local machine CPU/memory/swap/network/load for the stats header (system + network features only)

Dev Dependencies

  • Insta: Snapshot testing (use cargo insta accept to accept snapshots)
  • Mockall: Mock generation for testing

Performance Considerations

  • UI refresh rate is throttled to 500ms to reduce CPU usage
  • Event processing uses timeout-based throttling: waits for first event with timeout, then drains all pending
  • Container stats streams run independently per container across all hosts
  • Each host's container manager runs independently without blocking other hosts
  • Keyboard polling is 200ms to balance responsiveness and CPU
  • Styles are pre-allocated in UiStyles::default() to avoid allocations during rendering
  • Container references (not clones) are used when building UI rows
  • Container sorting is throttled to once every 3 seconds to avoid re-sorting on every render frame (stats updates are constant)
    • User-initiated sort changes (field selection, search, toggle filters) bypass throttle for immediate response
    • Container add/remove events also bypass throttle to maintain correctness
    • This reduces sorting from ~2/sec to ~0.33/sec during normal operation (~83% reduction)
  • Exponential smoothing (alpha=0.3) reduces noise in stats without heavy computation
  • Failed host connections are logged but don't prevent other hosts from being monitored
  • A host that drops after connecting is retried in the background with a capped backoff (1s → 5s), so a Docker daemon restart doesn't require restarting dtop
  • Log streaming is only active when viewing a container's logs (stopped when exiting log view)
  • Log text is formatted once when received and cached in AppState::formatted_log_text to avoid re-parsing ANSI codes on every frame
  • ANSI parsing happens at log arrival time, not render time

User Interactions

Container List View:

  • ↑/↓ - Navigate between containers
  • PageUp/PageDown (or Ctrl-U/Ctrl-D) - Move selection up/down by one page
  • Home/End - Jump to first/last container
  • Enter - Open action menu for selected container (or the bulk action menu when containers are marked)
  • Space - Mark/unmark the selected container for a bulk action (and move down)
  • Ctrl-A - Mark every listed container, or unmark them if all are marked (under tmux/screen with the default prefix, press the prefix then a to send it through)
  • p - Mark every listed container in the selected container's compose project on the same host, or unmark them if all are marked
  • Esc - Clear marks
  • →/l - View logs for selected container
  • q or Ctrl-C - Quit application
  • o - Open Dozzle for selected container (if configured and not in SSH session)
  • ? - Toggle help popup
  • / - Enter search mode (filter containers)
  • s - Open sort selector popup (Uptime, Name, CPU, Memory)
  • c - Open column visibility selector
  • t - Show/hide the system stats header
  • a - Toggle showing all containers (including stopped containers)
  • Ctrl-S - Save preferences (columns, sort, all, stats header) to config file
  • Ctrl-R - Reset preferences to defaults (requires y/n confirmation)

Log View:

  • ↑/↓ - Scroll through logs manually
  • ←/h or Esc - Return to container list
  • ? - Toggle help popup
  • Auto-scroll behavior: Automatically scrolls to bottom when new logs arrive (unless manually scrolled up)

Action Menu:

  • ↑/↓ - Navigate between available actions
  • Enter - Execute selected action
  • Esc - Cancel and return to container list
  • Available actions depend on container state (e.g., running containers can be stopped/restarted)

Search Mode:

  • Type to filter containers by name or ID (case-insensitive)
  • Backspace/Delete - Edit search query
  • Enter or Esc - Exit search mode
  • ↑/↓ - Navigate filtered results while searching

Testing Strategy

The codebase includes unit tests for:

  • Stats calculation logic (docker/stats.rs): CPU percentage, memory percentage, network rates, disk I/O rates, edge cases
  • UI color coding (ui/render.rs): Threshold boundaries for green/yellow/red
  • Log parsing (docker/logs.rs): Timestamp parsing, message extraction, edge cases
  • Config loading (cli/config.rs): YAML deserialization, CLI merging, host configurations
  • UI snapshot tests (ui/ui_tests.rs): Visual regression testing using insta

Run tests with cargo test or cargo insta test for snapshot tests.

Scope Discipline

Small first versions win. Building is cheap here; reviewing and maintaining is not.

  • Agree the scope as a changelog before any code. For a new feature, write what ships (user-visible bullets) and an explicit "Not in this release" list, and get it approved. Anything not on the ship list is out of scope, however reasonable it looks.
  • A plan is a menu, not a work order. Build phase 1 only. Later phases start after phase 1 has merged, unless the user asks for more.
  • Reuse before you add. A new config key, CLI flag, AppEvent, ViewState, key binding, column or popup needs a one-line reason why an existing one can't do it. Prefer extending an existing view (e.g. the action menu) to adding a new one.
  • Stop at ~1,000 lines. If a PR's non-test, non-snapshot diff passes ~1,000 lines, stop and check scope with the user before continuing.
  • One feature, one PR. Unrelated work (docs, refactors, a fix spotted on the way) gets its own PR off main, never a layer in the same branch. Avoid stacks deeper than two; with a GitHub stack, merge from the bottom.
  • Subagents and workflows get the out-of-scope list. Every implementation prompt includes "not in scope" and the rule: a deviation that adds behaviour is reported, not built.
  • Review fixes fix; they don't extend. Addressing feedback must not add new mechanisms. If a fix needs one, raise it as a scope question.
  • The automated review always finds something. claude-review re-reviews every push and lists items even when it says LGTM. Fix the bugs it finds; answer nits, suggestions and "consider" items once in a reply, without pushing for them. A round with no bugs ends the review loop. Fixes made under review are the likeliest place for new bugs, so keep them minimal.
  • Audit size before review, not after. When a body of work is done, report its size per feature (code, tests, snapshots) next to its user impact, so cuts happen before review time is spent.

Pre-Commit Checklist

IMPORTANT: These checks MUST pass before creating any PR or commit.

Before committing or submitting a pull request, always run:

  1. cargo fmt --check - Verify formatting (run cargo fmt to fix)
  2. cargo clippy - Ensure no warnings
  3. cargo test - Ensure all tests pass
  4. cargo insta accept - Accept any updated snapshots (if applicable)

Claude PR Review Guidelines

When reviewing pull requests for this repository, follow these guidelines:

  • Keep feedback concise and actionable - Focus on what matters, not lengthy explanations
  • Prioritize issues by severity:
    1. Security vulnerabilities (always report)
    2. Bugs and logic errors
    3. Performance issues
    4. API/interface issues
  • Skip minor nitpicks - Don't comment on trivial style issues unless they significantly affect readability
  • Be direct - Use short, clear sentences. Avoid filler phrases
  • Group related feedback - Don't create separate comments for each small issue
  • Limit comments to 3-5 items unless there are critical issues requiring more attention

Download now (ZIP)

Trending

Less talk lately.about the whole repository

Dec 2025: 0 mentionsJan 2026: 1 mentionFeb 2026: 0 mentionsMar 2026: 2 mentionsApr 2026: 2 mentionsMay 2026: 9 mentionsJun 2026: 5 mentionsJul 2026: 8 mentionsAug 2026: 9 mentionsSep 2026: 9 mentionsOct 2026: 9 mentionsNov 2026: 0 mentions
Dec 2025Jun 2026Nov 2026
Mentions a month of the amir20/dtop repository that our weekly crawl found. There is no adoption data for this file yet.

Who's talking about amir20/dtop

The biggest 5 of 54 mentions linking to the repository that our crawl found. Each line is their own headline.

See all 54

Talked about on GitHub and Bluesky (54 times).

On GitHub · 49

44 more on GitHub

On Bluesky · 5

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.