use core::fmt::Write as _;
use std::io::IsTerminal;
#[ derive( Debug, Clone ) ]
pub struct CliHelpStyle
{
pub cmd_indent : usize,
pub cmd_name_width : usize,
pub grp_indent : usize,
pub opt_indent : usize,
pub opt_name_width : usize,
pub col_gap : usize,
pub example_indent : usize,
pub color_tagline : &'static str,
pub color_group : &'static str,
pub color_option : &'static str,
pub color_example : &'static str,
pub color_reset : &'static str,
pub tty_detect : bool,
}
impl Default for CliHelpStyle
{
#[ inline ]
fn default() -> Self
{
Self
{
cmd_indent : 4,
cmd_name_width : 20,
grp_indent : 2,
opt_indent : 2,
opt_name_width : 18,
col_gap : 2,
example_indent : 2,
color_tagline : "\x1b[1m",
color_group : "\x1b[33m\x1b[1m",
color_option : "\x1b[1;36m",
color_example : "\x1b[2m",
color_reset : "\x1b[0m",
tty_detect : true,
}
}
}
#[ derive( Debug, Clone ) ]
pub struct CommandGroup
{
pub name : String,
pub entries : Vec<CommandEntry>,
}
#[ derive( Debug, Clone ) ]
pub struct CommandEntry
{
pub name : String,
pub desc : String,
}
#[ derive( Debug, Clone ) ]
pub struct OptionEntry
{
pub name : String,
pub desc : String,
}
#[ derive( Debug, Clone ) ]
pub struct ExampleEntry
{
pub invocation : String,
pub desc : Option<String>,
}
#[ derive( Debug, Clone ) ]
pub struct OptionGroup
{
pub name : String,
pub entries : Vec<OptionEntry>,
}
#[ non_exhaustive ]
#[ derive( Default, Debug, Clone ) ]
pub struct CliHelpData
{
pub binary : String,
pub tagline : String,
pub groups : Vec<CommandGroup>,
pub options : Vec<OptionEntry>,
pub examples : Vec<ExampleEntry>,
pub usage_lines : Vec<String>,
pub arguments : Vec<OptionEntry>,
pub option_groups : Vec<OptionGroup>,
}
#[ derive( Debug ) ]
pub struct CliHelpTemplate
{
style : CliHelpStyle,
data : CliHelpData,
}
impl CliHelpTemplate
{
#[ inline ]
#[ must_use ]
pub fn new( style : CliHelpStyle, data : CliHelpData ) -> Self
{
Self { style, data }
}
#[ inline ]
#[ must_use ]
pub fn render( &self ) -> String
{
let use_color = self.style.tty_detect && std::io::stdout().is_terminal();
let s = &self.style;
let c = | code : &'static str | -> &str { if use_color { code } else { "" } };
let bold = c( s.color_tagline );
let grp = c( s.color_group );
let opt = c( s.color_option );
let ex = c( s.color_example );
let rst = c( s.color_reset );
let mut out = String::new();
self.emit_header( &mut out, bold, rst );
self.emit_arguments( &mut out, bold, opt, rst );
self.emit_groups( &mut out, grp, opt, rst );
self.emit_option_groups( &mut out, bold, opt, rst );
if self.data.option_groups.is_empty() && !self.data.options.is_empty()
{
self.emit_options( &mut out, bold, opt, rst );
}
if !self.data.examples.is_empty() { self.emit_examples( &mut out, bold, ex, rst ); }
out
}
fn emit_header( &self, out : &mut String, bold : &str, rst : &str )
{
if !self.data.usage_lines.is_empty()
{
for line in &self.data.usage_lines
{
let _ = writeln!( out, " {line}" );
}
}
else
{
let _ = writeln!( out, "{bold}Usage:{rst} {} <command>", self.data.binary );
}
let _ = writeln!( out );
let _ = writeln!( out, "{}", self.data.tagline );
let _ = writeln!( out );
let _ = writeln!( out, "{bold}Commands:{rst}" );
}
fn emit_arguments( &self, out : &mut String, bold : &str, opt_color : &str, rst : &str )
{
if self.data.arguments.is_empty() { return; }
let max_len = self.data.arguments.iter().map( |e| e.name.len() ).max().unwrap_or( 0 );
let _ = writeln!( out, "\n{bold}Arguments:{rst}" );
for e in &self.data.arguments
{
let _ = writeln!( out, " {opt_color}{:<width$}{rst} {}", e.name, e.desc, width = max_len );
}
}
fn emit_groups( &self, out : &mut String, grp_color : &str, opt_color : &str, rst : &str )
{
let s = &self.style;
let gi = " ".repeat( s.grp_indent );
let ci = " ".repeat( s.cmd_indent );
let gp = " ".repeat( s.col_gap );
for group in &self.data.groups
{
let _ = writeln!( out, "\n{gi}{grp_color}{}{rst}", group.name );
for entry in &group.entries
{
let padded = format!( "{:<width$}", entry.name, width = s.cmd_name_width );
let _ = writeln!( out, "{ci}{opt_color}{padded}{rst}{gp}{}", entry.desc );
}
}
}
fn emit_option_group( &self, out : &mut String, bold : &str, opt_color : &str, rst : &str,
name : &str, entries : &[ OptionEntry ] )
{
if entries.is_empty() { return; }
let max_len = entries.iter().map( |e| e.name.len() ).max().unwrap_or( 0 );
let _ = writeln!( out, "\n{bold}{name}:{rst}" );
for e in entries
{
let _ = writeln!( out, " {opt_color}{:<width$}{rst} {}", e.name, e.desc, width = max_len );
}
}
fn emit_option_groups( &self, out : &mut String, bold : &str, opt_color : &str, rst : &str )
{
for group in &self.data.option_groups
{
self.emit_option_group( out, bold, opt_color, rst, &group.name, &group.entries );
}
}
fn emit_options( &self, out : &mut String, bold : &str, opt_color : &str, rst : &str )
{
let s = &self.style;
let oi = " ".repeat( s.opt_indent );
let gp = " ".repeat( s.col_gap );
let _ = writeln!( out );
let _ = writeln!( out, "{bold}Options:{rst}" );
for opt in &self.data.options
{
let padded = format!( "{:<width$}", opt.name, width = s.opt_name_width );
let _ = writeln!( out, "{oi}{opt_color}{padded}{rst}{gp}{}", opt.desc );
}
}
fn emit_examples( &self, out : &mut String, bold : &str, ex_color : &str, rst : &str )
{
let s = &self.style;
let ei = " ".repeat( s.example_indent );
let _ = writeln!( out );
let _ = writeln!( out, "{bold}Examples:{rst}" );
for ex in &self.data.examples
{
if let Some( ref desc ) = ex.desc
{
let _ = writeln!( out, "{ei}{ex_color}{} # {desc}{rst}", ex.invocation );
}
else
{
let _ = writeln!( out, "{ei}{ex_color}{}{rst}", ex.invocation );
}
}
}
}
#[ doc( inline ) ]
#[ allow( unused_imports ) ]
pub use own::*;
#[ allow( unused_imports ) ]
pub mod own
{
#[ allow( unused_imports ) ]
use super::*;
pub use orphan::*;
}
#[ allow( unused_imports ) ]
pub mod orphan
{
#[ allow( unused_imports ) ]
use super::*;
pub use exposed::*;
}
#[ allow( unused_imports ) ]
pub mod exposed
{
#[ allow( unused_imports ) ]
use super::*;
pub use prelude::*;
}
#[ allow( unused_imports ) ]
pub mod prelude
{
#[ allow( unused_imports ) ]
use super::*;
pub use super::
{
CliHelpStyle,
CommandGroup,
CommandEntry,
OptionEntry,
ExampleEntry,
CliHelpData,
CliHelpTemplate,
OptionGroup,
};
}