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.
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);
}Every exported class and utility lives under jowi::tui and focuses on building simple styled DOM trees that render to ANSI terminals.
- Paragraph & Layout first –
Paragraphis the text leaf; it supportsstd::formatconstruction andno_newline()to keep the cursor on the same line.Layoutis a container of children.
auto p = tui::Paragraph("Row {}", 1).no_newline();
auto layout = tui::Layout{}.append_child(std::move(p));- Styling layouts – Use
DomStyleto indent and color a layout; chainindent(),bg(),fg(),effect(), oreffects(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 trees –
DomNode::paragraph(...)andDomNode::vstack(Layout)enable a builder pattern: chainappend_childonLayoutto 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 effects –
RgbColorprovides helpers likeRgbColor::red()/bright_blue().TextEffectcovers bold, underline, blink, reverse, etc.
auto error_color = tui::RgbColor::bright_red();
tui::DomStyle{}.effect(tui::TextEffect::UNDERLINE);- Formatting –
DomNodespecializesstd::formatter, sostd::format("{}", dom)orstd::print("{}", dom)produces ANSI-styled output.
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 (
LevelFilterwith comparison helpers, orNoFilter); - an emitter: writes bytes to a destination (
StdoutEmitter,StderrEmitter,EmptyEmitter,FileEmitter::open(...)), usually wrapped byStreamEmitter<SsEmitter>buffers. - Messages & levels – Wrap text with
Message(inheritsRawMessage) so formatting is deferred. UseLogLevel::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(...)orLogError::io_error(...). - Formatters – Pick how logs look.
ColorfulFormatterusesjowi.tuicolors,BwFormatterprints plain text,PlainFormatterwrites only the message body,EmptyFormatterdrops 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};NoFilterpasses 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});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 targetBuild apps, parse arguments, and wire behaviors.
- AppIdentity & AppVersion – Metadata attached to an
App. Parse versions viaAppVersion::from_string("1.2.3").
auto ver = cli::AppVersion::from_string("2.0.1").value();- ArgKey – Safely represent
-k/--keytokens; build withArgKey::make("--out")or parse viaArgKey::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 attachArgOptionsValidator,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), andpost_validate(key, args); wrap withadd_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 / ParsedArg –
ArgParser::parsewalksRawArgsand fillsParsedArg, which exposesarg()(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 helpersApp::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 parsing –
parse_arg<T>(std::string_view)is implemented for integers, floats,std::string,std::filesystem::path, andcli::AppVersion, returningstd::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.