Skip to content

jowillianto/jowi-cli

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

78 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

jowi::cli

A C++23 command-line toolkit built on standard-library facilities plus the in-repo jowi.generic utilities. Modules:

- jowi.cli      (core CLI builder)
- jowi.tui      (terminal UI primitives)
- jowi.crogger  (logging)

No other runtime dependencies are required.

Quick Start (Hello World)

import jowi.cli;
import jowi.tui;
#include <print>
namespace cli = jowi::cli;
namespace tui = jowi::tui;

cli::AppIdentity id{
  .name = "hello",
  .description = "Minimal example",
  .author = "You",
  .license = "MIT",
  .version = cli::AppVersion{0, 1, 0}
};

int main(int argc, const char** argv) {
  auto app = cli::App{id, argc, argv};
  app.add_argument("--message").help("What to print").optional();
  app.parse_args().value(); // or cli::App::expect(...)

  auto msg = app.args().first_of("--message").value_or("Hello, world!");
  auto dom = tui::DomNode::vstack(
    tui::Layout{}
      .append_child(tui::Paragraph("Hello, {}!", msg).no_newline()) // formatted, no newline
      .append_child(tui::Paragraph(" <- rendered with tui::Paragraph"))
  );
  std::print("{}", dom);
}

Terminal UI (module jowi.tui)

Every exported class and utility lives under jowi::tui and focuses on building simple styled DOM trees that render to ANSI terminals.

  • Paragraph & Layout firstParagraph is the text leaf; it supports std::format construction and no_newline() to keep the cursor on the same line. Layout is a container of children.
auto p = tui::Paragraph("Row {}", 1).no_newline();
auto layout = tui::Layout{}.append_child(std::move(p));
  • Styling layouts – Use DomStyle to indent and color a layout; chain indent(), bg(), fg(), effect(), or effects(range).
auto style = tui::DomStyle{}.indent(2).fg(tui::RgbColor::green());
auto styled = tui::Layout{}.style(style).append_child(tui::Paragraph("Indented text"));
  • Building DomNode treesDomNode::paragraph(...) and DomNode::vstack(Layout) enable a builder pattern: chain append_child on Layout to grow the tree, then wrap it.
auto dom = tui::DomNode::vstack(
  tui::Layout{}
    .style(style)
    .append_child(tui::Paragraph("Title"))
    .append_child(tui::DomNode::vstack(
      tui::Layout{}.style(tui::DomStyle{}.indent(2))
        .append_child(tui::Paragraph("Line A"))
        .append_child(tui::Paragraph("Line B"))
    ))
);
  • Colors and effectsRgbColor provides helpers like RgbColor::red() / bright_blue(). TextEffect covers bold, underline, blink, reverse, etc.
auto error_color = tui::RgbColor::bright_red();
tui::DomStyle{}.effect(tui::TextEffect::UNDERLINE);
  • FormattingDomNode specializes std::formatter, so std::format("{}", dom) or std::print("{}", dom) produces ANSI-styled output.

Logging (module jowi.crogger)

jowi.crogger offers a minimal logging pipeline: build a message, pass it through a logger, and emit to stdout/stderr or files. A Logger is composed of:

  • a formatter: shapes the log line and colors/styling (implementations: ColorfulFormatter, BwFormatter, PlainFormatter, EmptyFormatter);
  • a filter: decides whether to log (LevelFilter with comparison helpers, or NoFilter);
  • an emitter: writes bytes to a destination (StdoutEmitter, StderrEmitter, EmptyEmitter, FileEmitter::open(...)), usually wrapped by StreamEmitter<SsEmitter> buffers.
  • Messages & levels – Wrap text with Message (inherits RawMessage) so formatting is deferred. Use LogLevel::trace/debug/info/warn/error/critical() to tag severity.
crogger::Message msg{"User {} logged in", user};
  • LogError / LogErrorType – errors surfaced by formatters or emitters; create with LogError::format_error(...) or LogError::io_error(...).
  • Formatters – Pick how logs look. ColorfulFormatter uses jowi.tui colors, BwFormatter prints plain text, PlainFormatter writes only the message body, EmptyFormatter drops output.
crogger::Logger l;
l.set_formatter(crogger::BwFormatter{});
  • Filters – Gate logs by level using LevelFilter::{equal_to,less_than,greater_than_or_equal_to}; NoFilter passes everything.
l.set_filter(crogger::LevelFilter::greater_than_or_equal_to(crogger::LogLevel::info().level));
  • Emitters – Send bytes to stdout/stderr, nothingness, or a file.
auto file = crogger::FileEmitter::open("app.log", true).value();
crogger::Logger custom;
custom.set_emitter(std::move(file));
  • Logger usage – Configure formatter/filter/emitter, then log via crogger::log(logger, level, message).
crogger::Logger l;
l.set_filter(crogger::LevelFilter::greater_than_or_equal_to(30))
 .set_formatter(crogger::ColorfulFormatter{})
 .set_emitter(crogger::StdoutEmitter{});
crogger::log(l, crogger::LogLevel::warn(), crogger::Message{"Low disk: {}%", 12});

Root logger

crogger::root() returns a process-wide logger. Helper functions trace/debug/info/warn/error/critical accept either a Logger or default to the root logger; customize the root once and reuse it everywhere.

using namespace jowi::crogger;
root().set_formatter(ColorfulFormatter{});
info(Message{"Starting {}", id.name});    // uses root()
error(root(), Message{"Failure: {}", reason}); // explicit logger target

CLI (module jowi.cli)

Build apps, parse arguments, and wire behaviors.

  • AppIdentity & AppVersion – Metadata attached to an App. Parse versions via AppVersion::from_string("1.2.3").
auto ver = cli::AppVersion::from_string("2.0.1").value();
  • ArgKey – Safely represent -k/--key tokens; build with ArgKey::make("--out") or parse via ArgKey::parse_arg("--out=file").
auto [key, val] = cli::ArgKey::parse_arg("--name=john").value();
  • Arg & built-in validators – Chain helpers: required(), optional(), as_flag(), require_value(), n_at_least(...), with_default(...), or attach ArgOptionsValidator, ArgCountValidator, ArgEmptyValidator, ArgDefaultValidator.
app.add_argument("--mode", cli::Arg{}.require_value()
  .add_validator(cli::ArgOptionsValidator{}.add_option("fast").add_option("safe")));
  • Custom validators – Implement any subset of id(), help(), validate(value), and post_validate(key, args); wrap with add_validator.
struct PositiveInt {
  std::optional<std::string> id() const { return "positive_int"; }
  std::expected<void, cli::ParseError> validate(std::optional<std::string_view> v) const {
    auto num = cli::parse_arg<int>(*v);
    if (!num || num.value() <= 0) return std::unexpected{cli::ParseError::invalid_value("must be > 0")};
    return {};
  }
};
app.add_argument("--count").add_validator(PositiveInt{});
  • ArgParser / ParsedArgArgParser::parse walks RawArgs and fills ParsedArg, which exposes arg() (positional), first_of(key), contains(key), count(key), and iteration over captured pairs.
if (app.args().contains("--verbose")) { /* ... */ }
  • App – Central type that wires the parser, help output, and error handling. Use add_argument(...), parse_args(auto_help=true, auto_exit=true), and helpers App::expect(...), App::expect_or(...), App::error(...). help_dom() renders structured help using the TUI stack.
app.add_argument("-v").as_flag().help("Verbose");
cli::App::expect(app.parse_args(), "Failed: {}");
  • ActionBuilder – Build subcommands/actions for a positionally-chosen verb and enforce allowed options automatically.
cli::ActionBuilder builder{app, "Choose an action"};
builder.add_action("serve", "Start server", [](cli::App& app){ /* ... */ })
       .add_action("migrate", "Run migrations", [](cli::App& app){ /* ... */ })
       .update_args() // sync help/options
       .run();        // parses args and dispatches
  • Shortcut parsingparse_arg<T>(std::string_view) is implemented for integers, floats, std::string, std::filesystem::path, and cli::AppVersion, returning std::expected<T, ParseError>.
auto port = cli::parse_arg<int>("8080").value();

With these pieces you can compose styled terminal output, structured logging, and ergonomic argument parsing within a single module-first C++23 codebase.

About

C++23 based library to write command line based applications.

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published