diff --git a/debian-bin/dcps b/debian-bin/dcps new file mode 100644 index 0000000..fe08d7d --- /dev/null +++ b/debian-bin/dcps @@ -0,0 +1,2 @@ +#!/bin/sh +/usr/bin/enderclitools dcps "$@" diff --git a/src/args/dcps.rs b/src/args/dcps.rs new file mode 100644 index 0000000..7c26763 --- /dev/null +++ b/src/args/dcps.rs @@ -0,0 +1,56 @@ +use crate::config::model::dcps::DcpsHeader; +use crate::config::model::table::{TableModifiers, TablePresets}; +use clap::{Args, ValueEnum}; +use std::fmt; + +#[derive(Args, Debug, Clone)] +/// Pretty replacement for `docker compose ps` +pub struct DcpsArgs { + /// Show all containers (default shows just running) + #[arg(short, long)] + pub all: bool, + /// Filter output based on conditions provided + #[arg(short, long)] + pub filter: Option, + /// Don't truncate output + #[arg(long)] + pub no_trunc: bool, + /// Exclude orphaned services (not declared by project) + #[arg(long)] + pub no_orphans: bool, + /// Only display container IDs + #[arg(short, long)] + pub quiet: bool, + /// Display services + #[arg(long)] + pub services: bool, + /// Filter services by status. + #[arg(long)] + pub status: Option>, + #[arg(long, value_enum)] + pub table_preset: Option, + #[arg(long, value_enum)] + pub table_modifier: Option, + #[arg(long, value_enum)] + pub headers: Option>, + #[arg(long, value_enum)] + pub add_headers: Option>, +} + +#[derive(Debug, Clone, ValueEnum, Copy)] +pub enum Status { + Paused, + Restarting, + Removing, + Running, + Dead, + Created, + Exited, +} + +impl fmt::Display for Status { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let s = format!("{:?}", self).to_ascii_lowercase(); + write!(f, "{}", s) + } +} diff --git a/src/args/mod.rs b/src/args/mod.rs index 353af1e..f51068c 100644 --- a/src/args/mod.rs +++ b/src/args/mod.rs @@ -1,8 +1,10 @@ use crate::args::config::ConfigArgs; +use crate::args::dcps::DcpsArgs; use crate::args::dps::DpsArgs; use clap::{Parser, Subcommand}; pub mod config; +pub mod dcps; pub mod dps; #[derive(Parser, Debug)] @@ -26,5 +28,6 @@ impl Cli { #[derive(Subcommand, Debug, Clone)] pub enum Commands { Dps(DpsArgs), + Dcps(DcpsArgs), Config(ConfigArgs), } diff --git a/src/cmd/dcps.rs b/src/cmd/dcps.rs new file mode 100644 index 0000000..3538825 --- /dev/null +++ b/src/cmd/dcps.rs @@ -0,0 +1,74 @@ +use crate::args::dcps::DcpsArgs; +use crate::config::Config; +use crate::config::model::dcps::DcpsHeader; +use crate::utils; +use crate::utils::table::TableRow; +use anyhow::Result; + +pub fn run(args: DcpsArgs) -> Result<()> { + let stdout = utils::docker::compose::ps( + args.all, + args.headers.as_deref(), + args.add_headers.as_deref(), + args.no_trunc, + args.no_orphans, + args.quiet, + args.services, + args.status.as_deref(), + )?; + let cfg = Config::load()?; + let table_preset = args.table_preset.unwrap_or(cfg.table.preset); + let table_modifier = args.table_modifier.unwrap_or(cfg.table.modifier); + + let mut table_headers: TableRow = if args.quiet { + vec![DcpsHeader::Id.display_name().into()] + } else if args.services { + vec![DcpsHeader::Service.display_name().into()] + } else { + if let Some(headers) = args.headers { + headers + .into_iter() + .map(|h| h.display_name().into()) + .collect() + } else { + cfg.dcps + .headers + .iter() + .map(|h| h.display_name().into()) + .collect() + } + }; + + if let Some(add_headers) = args.add_headers { + for add_header in add_headers { + table_headers.push(add_header.display_name().into()); + } + }; + + let mut table = utils::table::build_table( + &table_headers, + None, + Some(&table_preset), + Some(&table_modifier), + ); + + for line in stdout.lines() { + if line.trim().is_empty() { + continue; + } + + let mut cols = line + .split(';') + .map(|s| s.trim().into()) + .collect::(); + while cols.len() < table_headers.len() { + cols.push("".into()); + } + + table.add_row(cols); + } + + println!("{}", table); + + Ok(()) +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index f45e407..d761e0c 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1,2 +1,3 @@ pub mod config; +pub mod dcps; pub mod dps; diff --git a/src/config/model/dcps.rs b/src/config/model/dcps.rs new file mode 100644 index 0000000..94e3480 --- /dev/null +++ b/src/config/model/dcps.rs @@ -0,0 +1,63 @@ +use clap::ValueEnum; +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct DcpsConfig { + pub headers: Vec, +} + +impl Default for DcpsConfig { + fn default() -> DcpsConfig { + DcpsConfig { + headers: vec![ + DcpsHeader::Service, + DcpsHeader::Image, + DcpsHeader::Status, + DcpsHeader::Ports, + ], + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, ValueEnum)] +pub enum DcpsHeader { + Id, + Service, + Names, + Image, + Status, + Ports, + Command, + CreatedAt, + Created, + Size, + Labels, + Mounts, +} + +impl fmt::Display for DcpsHeader { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl DcpsHeader { + pub fn display_name(&self) -> &str { + match self { + DcpsHeader::Id => "ID", + DcpsHeader::Service => "Service", + DcpsHeader::Names => "Names", + DcpsHeader::Image => "Image", + DcpsHeader::Status => "Status", + DcpsHeader::Ports => "Ports", + DcpsHeader::Command => "Command", + DcpsHeader::CreatedAt => "CreatedAt", + DcpsHeader::Created => "RunningFor", + DcpsHeader::Size => "Size", + DcpsHeader::Labels => "Labels", + DcpsHeader::Mounts => "Mounts", + } + } +} diff --git a/src/config/model/dps.rs b/src/config/model/dps.rs index d72b2d8..f09175c 100644 --- a/src/config/model/dps.rs +++ b/src/config/model/dps.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use std::fmt; #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] pub struct DpsConfig { pub headers: Vec, } diff --git a/src/config/model/mod.rs b/src/config/model/mod.rs index 0f3ca02..142ad04 100644 --- a/src/config/model/mod.rs +++ b/src/config/model/mod.rs @@ -1,12 +1,16 @@ +use crate::config::model::dcps::DcpsConfig; use dps::DpsConfig; use serde::{Deserialize, Serialize}; use table::TableConfig; +pub mod dcps; pub mod dps; pub mod table; #[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(default)] pub struct Config { pub table: TableConfig, pub dps: DpsConfig, + pub dcps: DcpsConfig, } diff --git a/src/config/model/table.rs b/src/config/model/table.rs index 2cb0fd0..1b16351 100644 --- a/src/config/model/table.rs +++ b/src/config/model/table.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use std::fmt; #[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] +#[serde(default)] pub struct TableConfig { pub preset: TablePresets, pub modifier: TableModifiers, diff --git a/src/main.rs b/src/main.rs index 1236a95..3304669 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,9 @@ fn main() -> Result<()> { args::Commands::Dps(opts) => { cmd::dps::run(opts)?; } + args::Commands::Dcps(opts) => { + cmd::dcps::run(opts)?; + } args::Commands::Config(opts) => { cmd::config::run(opts)?; } diff --git a/src/utils/docker/compose.rs b/src/utils/docker/compose.rs new file mode 100644 index 0000000..3cc1a80 --- /dev/null +++ b/src/utils/docker/compose.rs @@ -0,0 +1,86 @@ +use crate::args::dcps::Status; +use crate::config::Config; +use crate::config::model::dcps::DcpsHeader; +use anyhow::{Context, Result, bail}; +use std::io; +use std::process::Command; + +#[allow(clippy::too_many_arguments)] +pub fn ps( + all: bool, + headers: Option<&[DcpsHeader]>, + add_headers: Option<&[DcpsHeader]>, + no_trunc: bool, + no_orphans: bool, + quiet: bool, + services: bool, + status: Option<&[Status]>, +) -> Result { + let mut args = vec!["compose".into(), "ps".to_string()]; + + if all { + args.push("--all".into()); + } + if no_trunc { + args.push("--no-trunc".into()); + } + if no_orphans { + args.push("--orphans=false".into()); + } + if let Some(status) = status { + for s in status { + args.push("--status".into()); + args.push(s.to_string()); + } + } + + args.push("--format".to_string()); + let base_headers: Option<&[DcpsHeader]> = if quiet { + Some(&[DcpsHeader::Id]) + } else if services { + Some(&[DcpsHeader::Service]) + } else { + headers + }; + let extra_headers = if quiet | services { None } else { add_headers }; + args.push(get_headers(base_headers, extra_headers)?); + + let attempt = Command::new("docker").args(&args).output(); + + match attempt { + Ok(out) if out.status.success() => Ok(String::from_utf8_lossy(&out.stdout).to_string()), + _ => { + if atty::is(atty::Stream::Stdin) { + bail!( + "failed to run `docker compose {}` and so STDIN provided", + args.join(" ") + ) + } + let mut buf = String::new(); + io::stdin().read_line(&mut buf).context("reading STDIN")?; + Ok(buf) + } + } +} + +fn get_headers( + headers: Option<&[DcpsHeader]>, + add_headers: Option<&[DcpsHeader]>, +) -> Result { + fn build(h: &[DcpsHeader], extra: Option<&[DcpsHeader]>) -> String { + h.iter() + .chain(extra.unwrap_or_default().iter()) + .map(|hdr| format!("{{{{.{}}}}}", hdr.display_name())) + .collect::>() + .join(";") + } + + let result = if let Some(h) = headers { + build(h, add_headers) + } else { + let cfg = Config::load()?; + build(&cfg.dcps.headers, add_headers) + }; + + Ok(result) +} diff --git a/src/utils/docker.rs b/src/utils/docker/mod.rs similarity index 99% rename from src/utils/docker.rs rename to src/utils/docker/mod.rs index 798d0b5..475f70a 100644 --- a/src/utils/docker.rs +++ b/src/utils/docker/mod.rs @@ -1,3 +1,5 @@ +pub mod compose; + use crate::config::Config; use crate::config::model::dps::DpsHeader; use anyhow::{Context, Result, bail}; diff --git a/wix/alias/dcps.cmd b/wix/alias/dcps.cmd new file mode 100644 index 0000000..459555b --- /dev/null +++ b/wix/alias/dcps.cmd @@ -0,0 +1,2 @@ +@echo off +"%~dp0EnderCliTools.exe" dcps %* diff --git a/wix/main.wxs b/wix/main.wxs index d01f8ee..fe6145c 100644 --- a/wix/main.wxs +++ b/wix/main.wxs @@ -129,6 +129,9 @@ + + + @@ -154,6 +157,7 @@ +