diff --git a/Makefile b/Makefile index 990fa64f..33373c8c 100644 --- a/Makefile +++ b/Makefile @@ -52,6 +52,7 @@ all: rgbasm rgblink rgbfix rgbgfx common_obj := \ src/extern/getopt.o \ src/diagnostics.o \ + src/style.o \ src/usage.o rgbasm_obj := \ diff --git a/include/platform.hpp b/include/platform.hpp index f8e55cc3..d9979f65 100644 --- a/include/platform.hpp +++ b/include/platform.hpp @@ -28,6 +28,7 @@ #define STDERR_FILENO 2 #define ssize_t int #define SSIZE_MAX INT_MAX + #define isatty _isatty #else #include // IWYU pragma: export #include // IWYU pragma: export diff --git a/include/style.hpp b/include/style.hpp new file mode 100644 index 00000000..c64205fe --- /dev/null +++ b/include/style.hpp @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +#ifndef RGBDS_STYLE_HPP +#define RGBDS_STYLE_HPP + +#include + +#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__CYGWIN__) + #define STYLE_ANSI 0 +#else + #define STYLE_ANSI 1 +#endif + +enum StyleColor { +#if STYLE_ANSI + // Values analogous to ANSI foreground and background SGR colors + STYLE_BLACK, + STYLE_RED, + STYLE_GREEN, + STYLE_YELLOW, + STYLE_BLUE, + STYLE_MAGENTA, + STYLE_CYAN, + STYLE_GRAY, +#else + // Values analogous to `FOREGROUND_*` constants from `windows.h` + STYLE_BLACK, + STYLE_BLUE, // bit 0 + STYLE_GREEN, // bit 1 + STYLE_CYAN, // STYLE_BLUE | STYLE_GREEN + STYLE_RED, // bit 2 + STYLE_MAGENTA, // STYLE_BLUE | STYLE_RED + STYLE_YELLOW, // STYLE_GREEN | STYLE_RED + STYLE_GRAY, // STYLE_BLUE | STYLE_GREEN | STYLE_RED +#endif +}; + +void style_Enable(bool enable); +void style_Set(FILE *file, StyleColor color, bool bold); +void style_Reset(FILE *file); + +#endif // RGBDS_STYLE_HPP diff --git a/include/usage.hpp b/include/usage.hpp index 5ebfcc19..d9e45725 100644 --- a/include/usage.hpp +++ b/include/usage.hpp @@ -4,12 +4,14 @@ #define RGBDS_USAGE_HPP #include +#include +#include +#include -class Usage { - char const *usage; - -public: - Usage(char const *usage_) : usage(usage_) {} +struct Usage { + std::string name; + std::vector flags; + std::vector, std::vector>> options; [[noreturn]] void printAndExit(int code) const; diff --git a/man/rgbasm.1 b/man/rgbasm.1 index ca725dac..553ac306 100644 --- a/man/rgbasm.1 +++ b/man/rgbasm.1 @@ -10,6 +10,7 @@ .Nm .Op Fl EhVvw .Op Fl b Ar chars +.Op Fl \-color Ar when .Op Fl D Ar name Ns Op = Ns Ar value .Op Fl g Ar chars .Op Fl I Ar path @@ -65,6 +66,18 @@ letters, .Sq # , or .Sq @ . +.It Fl \-color Ar when +Specify when to highlight warning and error messages with color: +.Ql always , +.Ql never , +or +.Ql auto . +.Ql auto +determines whether to use colors based on the +.Ql Lk https://no-color.org/ NO_COLOR +or +.Ql Lk https://force-color.org/ FORCE_COLOR +environment variables, or whether the output is to a TTY. .It Fl D Ar name Ns Oo = Ns Ar value Oc , Fl \-define Ar name Ns Oo = Ns Ar value Oc Add a string symbol to the compiled source code. This is equivalent to diff --git a/man/rgbfix.1 b/man/rgbfix.1 index 3fca22bc..6e03bd16 100644 --- a/man/rgbfix.1 +++ b/man/rgbfix.1 @@ -10,6 +10,7 @@ .Nm .Op Fl hjOsVvw .Op Fl C | c +.Op Fl \-color Ar when .Op Fl f Ar fix_spec .Op Fl i Ar game_id .Op Fl k Ar licensee_str @@ -46,13 +47,13 @@ can be a path to a file, or to read from standard input. .Pp Note that options can be abbreviated as long as the abbreviation is unambiguous: -.Fl \-color-o +.Fl \-verb is -.Fl \-color-only , +.Fl \-verbose , but -.Fl \-color +.Fl \-ver is invalid because it could also be -.Fl \-color-compatible . +.Fl \-version . Options later in the command line override those set earlier. Accepted options are as follows: .Bl -tag -width Ds @@ -70,6 +71,18 @@ to 0x80. This overrides .Fl c if it was set prior. +.It Fl \-color Ar when +Specify when to highlight warning and error messages with color: +.Ql always , +.Ql never , +or +.Ql auto . +.Ql auto +determines whether to use colors based on the +.Ql Lk https://no-color.org/ NO_COLOR +or +.Ql Lk https://force-color.org/ FORCE_COLOR +environment variables, or whether the output is to a TTY. .It Fl f Ar fix_spec , Fl \-fix-spec Ar fix_spec Fix certain header values that the Game Boy checks for correctness. Alternatively, intentionally trash these values by writing their binary inverse instead. diff --git a/man/rgbgfx.1 b/man/rgbgfx.1 index db4c3cf1..359290d6 100644 --- a/man/rgbgfx.1 +++ b/man/rgbgfx.1 @@ -15,6 +15,7 @@ .Op Fl a Ar attrmap | Fl A .Op Fl b Ar base_ids .Op Fl c Ar pal_spec +.Op Fl \-color Ar when .Op Fl d Ar depth .Op Fl i Ar input_tiles .Op Fl L Ar slice @@ -195,6 +196,18 @@ See .Sx PALETTE SPECIFICATION FORMATS for a list of formats and their descriptions. .El +.It Fl \-color Ar when +Specify when to highlight warning and error messages with color: +.Ql always , +.Ql never , +or +.Ql auto . +.Ql auto +determines whether to use colors based on the +.Ql Lk https://no-color.org/ NO_COLOR +or +.Ql Lk https://force-color.org/ FORCE_COLOR +environment variables, or whether the output is to a TTY. .It Fl d Ar depth , Fl \-depth Ar depth Set the bit depth of the output tile data, in bits per pixel (bpp), either 1 or 2 (the default). This changes how tile data is output, and the maximum number of colors per palette (2 and 4 respectively). diff --git a/man/rgblink.1 b/man/rgblink.1 index 8bd1c72d..344f1ac8 100644 --- a/man/rgblink.1 +++ b/man/rgblink.1 @@ -9,6 +9,7 @@ .Sh SYNOPSIS .Nm .Op Fl dhMtVvwx +.Op Fl \-color Ar when .Op Fl l Ar linker_script .Op Fl m Ar map_file .Op Fl n Ar sym_file @@ -63,6 +64,18 @@ is invalid because it could also be .Fl \-version . The arguments are as follows: .Bl -tag -width Ds +.It Fl \-color Ar when +Specify when to highlight warning and error messages with color: +.Ql always , +.Ql never , +or +.Ql auto . +.Ql auto +determines whether to use colors based on the +.Ql Lk https://no-color.org/ NO_COLOR +or +.Ql Lk https://force-color.org/ FORCE_COLOR +environment variables, or whether the output is to a TTY. .It Fl d , Fl \-dmg Enable DMG mode. Prohibit the use of sections that doesn't exist on a DMG, such as VRAM bank 1. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 59a6dcf1..3979e90d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,6 +5,7 @@ configure_file(version.cpp _version.cpp ESCAPE_QUOTES) set(common_src "extern/getopt.cpp" "diagnostics.cpp" + "style.cpp" "usage.cpp" "_version.cpp" ) diff --git a/src/asm/charmap.cpp b/src/asm/charmap.cpp index d427efea..0249fb51 100644 --- a/src/asm/charmap.cpp +++ b/src/asm/charmap.cpp @@ -72,7 +72,7 @@ bool charmap_ForEach( }); mapFunc(charmap.name); - for (auto [nodeIdx, mapping] : mappings) { + for (auto const &[nodeIdx, mapping] : mappings) { charFunc(mapping, charmap.nodes[nodeIdx].value); } } diff --git a/src/asm/fstack.cpp b/src/asm/fstack.cpp index 0583024a..efecce4b 100644 --- a/src/asm/fstack.cpp +++ b/src/asm/fstack.cpp @@ -15,6 +15,7 @@ #include "helpers.hpp" #include "linkdefs.hpp" #include "platform.hpp" // S_ISDIR (stat macro) +#include "style.hpp" #include "verbosity.hpp" #include "asm/lexer.hpp" @@ -63,19 +64,27 @@ std::string const &FileStackNode::dump(uint32_t curLineNo) const { if (std::holds_alternative>(data)) { assume(parent); // REPT nodes use their parent's name std::string const &lastName = parent->dump(lineNo); + style_Set(stderr, STYLE_CYAN, false); fputs(" -> ", stderr); + style_Set(stderr, STYLE_CYAN, true); fputs(lastName.c_str(), stderr); fputs(reptChain().c_str(), stderr); + style_Set(stderr, STYLE_CYAN, false); fprintf(stderr, "(%" PRIu32 ")", curLineNo); + style_Reset(stderr); return lastName; } else { if (parent) { parent->dump(lineNo); + style_Set(stderr, STYLE_CYAN, false); fputs(" -> ", stderr); } std::string const &nodeName = name(); + style_Set(stderr, STYLE_CYAN, true); fputs(nodeName.c_str(), stderr); + style_Set(stderr, STYLE_CYAN, false); fprintf(stderr, "(%" PRIu32 ")", curLineNo); + style_Reset(stderr); return nodeName; } } diff --git a/src/asm/lexer.cpp b/src/asm/lexer.cpp index 082e9d4c..ef8a6b63 100644 --- a/src/asm/lexer.cpp +++ b/src/asm/lexer.cpp @@ -18,6 +18,7 @@ #include #include "helpers.hpp" +#include "style.hpp" #include "util.hpp" #include "verbosity.hpp" @@ -845,9 +846,15 @@ void lexer_DumpStringExpansions() { for (Expansion &exp : lexerState->expansions) { // Only register EQUS expansions, not string args if (exp.name) { - fprintf(stderr, "while expanding symbol \"%s\"\n", exp.name->c_str()); + style_Set(stderr, STYLE_CYAN, false); + fputs("while expanding symbol \"", stderr); + style_Set(stderr, STYLE_CYAN, true); + fputs(exp.name->c_str(), stderr); + style_Set(stderr, STYLE_CYAN, false); + fputs("\"\n", stderr); } } + style_Reset(stderr); } // Functions to discard non-tokenized characters diff --git a/src/asm/main.cpp b/src/asm/main.cpp index eb6781ad..1cd923cb 100644 --- a/src/asm/main.cpp +++ b/src/asm/main.cpp @@ -15,6 +15,7 @@ #include "extern/getopt.hpp" #include "helpers.hpp" #include "parser.hpp" // Generated from parser.y +#include "style.hpp" #include "usage.hpp" #include "util.hpp" // UpperMap #include "verbosity.hpp" @@ -36,7 +37,7 @@ static std::unordered_map> stateFileSpecs static char const *optstring = "b:D:Eg:hI:M:o:P:p:Q:r:s:VvW:wX:"; // Variables for the long-only options -static int depType; // Variants of `-M` +static int longOpt; // `--color` and variants of `-M` // Equivalent long options // Please keep in the same order as short opts. @@ -53,11 +54,6 @@ static option const longopts[] = { {"help", no_argument, nullptr, 'h'}, {"include", required_argument, nullptr, 'I'}, {"dependfile", required_argument, nullptr, 'M'}, - {"MC", no_argument, &depType, 'C'}, - {"MG", no_argument, &depType, 'G'}, - {"MP", no_argument, &depType, 'P'}, - {"MQ", required_argument, &depType, 'Q'}, - {"MT", required_argument, &depType, 'T'}, {"output", required_argument, nullptr, 'o'}, {"preinclude", required_argument, nullptr, 'P'}, {"pad-value", required_argument, nullptr, 'p'}, @@ -68,27 +64,34 @@ static option const longopts[] = { {"verbose", no_argument, nullptr, 'v'}, {"warning", required_argument, nullptr, 'W'}, {"max-errors", required_argument, nullptr, 'X'}, - {nullptr, no_argument, nullptr, 0 } + {"color", required_argument, &longOpt, 'c'}, + {"MC", no_argument, &longOpt, 'C'}, + {"MG", no_argument, &longOpt, 'G'}, + {"MP", no_argument, &longOpt, 'P'}, + {"MQ", required_argument, &longOpt, 'Q'}, + {"MT", required_argument, &longOpt, 'T'}, + {nullptr, no_argument, nullptr, 0 }, }; -// clang-format off: long string literal -static Usage usage( - "Usage: rgbasm [-EhVvw] [-b chars] [-D name[=value]] [-g chars] [-I path]\n" - " [-M depend_file] [-MC] [-MG] [-MP] [-MT target_file] [-MQ target_file]\n" - " [-o out_file] [-P include_file] [-p pad_value] [-Q precision]\n" - " [-r depth] [-s features:state_file] [-W warning] [-X max_errors]\n" - " \n" - "Useful options:\n" - " -E, --export-all export all labels\n" - " -M, --dependfile set the output dependency file\n" - " -o, --output set the output object file\n" - " -p, --pad-value set the value to use for `ds'\n" - " -s, --state : set an output state file\n" - " -V, --version print RGBASM version and exit\n" - " -W, --warning enable or disable warnings\n" - "\n" - "For help, use `man rgbasm' or go to https://rgbds.gbdev.io/docs/\n" -); +// clang-format off: nested initializers +static Usage usage = { + .name = "rgbasm", + .flags = { + "[-EhVvw]", "[-b chars]", "[-D name[=value]]", "[-g chars]", "[-I path]", + "[-M depend_file]", "[-MC]", "[-MG]", "[-MP]", "[-MT target_file]", "[-MQ target_file]", + "[-o out_file]", "[-P include_file]", "[-p pad_value]", "[-Q precision]", "[-r depth]", + "[-s features:state_file]", "[-W warning]", "[-X max_errors]", "", + }, + .options = { + {{"-E", "--export-all"}, {"export all labels"}}, + {{"-M", "--dependfile "}, {"set the output dependency file"}}, + {{"-o", "--output "}, {"set the output object file"}}, + {{"-p", "--pad-value "}, {"set the value to use for `ds'"}}, + {{"-s", "--state :"}, {"set an output state file"}}, + {{"-V", "--version"}, {"print RGBASM version and exit"}}, + {{"-W", "--warning "}, {"enable or disable warnings"}}, + }, +}; // clang-format on // LCOV_EXCL_START @@ -160,7 +163,7 @@ static void verboseOutputConfig(int argc, char *argv[]) { "char", "macro", }; - for (auto [name, features] : stateFileSpecs) { + for (auto const &[name, features] : stateFileSpecs) { fprintf(stderr, "\t - %s: ", name == "-" ? "" : name.c_str()); for (size_t i = 0; i < features.size(); ++i) { if (i > 0) { @@ -449,7 +452,17 @@ int main(int argc, char *argv[]) { // Long-only options case 0: - switch (depType) { + switch (longOpt) { + case 'c': + if (!strcasecmp(musl_optarg, "always")) { + style_Enable(true); + } else if (!strcasecmp(musl_optarg, "never")) { + style_Enable(false); + } else if (strcasecmp(musl_optarg, "auto")) { + fatal("Invalid argument for option '--color'"); + } + break; + case 'C': options.missingIncludeState = GEN_CONTINUE; break; @@ -465,7 +478,7 @@ int main(int argc, char *argv[]) { case 'Q': case 'T': { std::string newTarget = musl_optarg; - if (depType == 'Q') { + if (longOpt == 'Q') { newTarget = escapeMakeChars(newTarget); } if (!options.targetFileName.empty()) { @@ -549,7 +562,7 @@ int main(int argc, char *argv[]) { out_WriteObject(); - for (auto [name, features] : stateFileSpecs) { + for (auto const &[name, features] : stateFileSpecs) { out_WriteState(name, features); } diff --git a/src/asm/warning.cpp b/src/asm/warning.cpp index 3d3cf042..01c7c9ab 100644 --- a/src/asm/warning.cpp +++ b/src/asm/warning.cpp @@ -12,6 +12,7 @@ #include "diagnostics.hpp" #include "helpers.hpp" #include "itertools.hpp" +#include "style.hpp" #include "asm/fstack.hpp" #include "asm/lexer.hpp" @@ -64,14 +65,24 @@ Diagnostics warnings = { // clang-format on static void printDiag( - char const *fmt, va_list args, char const *type, char const *flagfmt, char const *flag + char const *fmt, + va_list args, + char const *type, + StyleColor color, + char const *flagfmt, + char const *flag ) { - fputs(type, stderr); - fputs(": ", stderr); + style_Set(stderr, color, true); + fprintf(stderr, "%s: ", type); if (fstk_DumpCurrent()) { - fprintf(stderr, flagfmt, flag); + putc(':', stderr); + if (flagfmt) { + style_Set(stderr, color, true); + fprintf(stderr, flagfmt, flag); + } fputs("\n ", stderr); } + style_Reset(stderr); vfprintf(stderr, fmt, args); putc('\n', stderr); @@ -82,13 +93,16 @@ static void incrementErrors() { // This intentionally makes 0 act as "unlimited" warnings.incrementErrors(); if (warnings.nbErrors == options.maxErrors) { + style_Set(stderr, STYLE_RED, true); fprintf( stderr, - "Assembly aborted after the maximum of %" PRIu64 " error%s! (configure with " - "'-X/--max-errors')\n", + "Assembly aborted after the maximum of %" PRIu64 " error%s!", warnings.nbErrors, warnings.nbErrors == 1 ? "" : "s" ); + style_Set(stderr, STYLE_RED, false); + fputs(" (configure with '-X/--max-errors')\n", stderr); + style_Reset(stderr); exit(1); } } @@ -97,17 +111,19 @@ void error(char const *fmt, ...) { va_list args; va_start(args, fmt); - printDiag(fmt, args, "error", ":", nullptr); + printDiag(fmt, args, "error", STYLE_RED, nullptr, nullptr); va_end(args); incrementErrors(); } void error(std::function callback) { + style_Set(stderr, STYLE_RED, true); fputs("error: ", stderr); if (fstk_DumpCurrent()) { fputs(":\n ", stderr); } + style_Reset(stderr); callback(); putc('\n', stderr); lexer_DumpStringExpansions(); @@ -120,7 +136,7 @@ void fatal(char const *fmt, ...) { va_list args; va_start(args, fmt); - printDiag(fmt, args, "FATAL", ":", nullptr); + printDiag(fmt, args, "FATAL", STYLE_RED, nullptr, nullptr); va_end(args); exit(1); @@ -128,12 +144,14 @@ void fatal(char const *fmt, ...) { void requireZeroErrors() { if (warnings.nbErrors != 0) { + style_Set(stderr, STYLE_RED, true); fprintf( stderr, "Assembly aborted with %" PRIu64 " error%s!\n", warnings.nbErrors, warnings.nbErrors == 1 ? "" : "s" ); + style_Reset(stderr); exit(1); } } @@ -149,11 +167,11 @@ void warning(WarningID id, char const *fmt, ...) { break; case WarningBehavior::ENABLED: - printDiag(fmt, args, "warning", ": [-W%s]", flag); + printDiag(fmt, args, "warning", STYLE_YELLOW, " [-W%s]", flag); break; case WarningBehavior::ERROR: - printDiag(fmt, args, "error", ": [-Werror=%s]", flag); + printDiag(fmt, args, "error", STYLE_RED, " [-Werror=%s]", flag); break; } diff --git a/src/diagnostics.cpp b/src/diagnostics.cpp index c0a64a01..f207a969 100644 --- a/src/diagnostics.cpp +++ b/src/diagnostics.cpp @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: MIT + #include "diagnostics.hpp" void warnx(char const *fmt, ...) { diff --git a/src/fix/main.cpp b/src/fix/main.cpp index 132a24dd..6e3da2fc 100644 --- a/src/fix/main.cpp +++ b/src/fix/main.cpp @@ -16,6 +16,7 @@ #include "extern/getopt.hpp" #include "helpers.hpp" #include "platform.hpp" +#include "style.hpp" #include "usage.hpp" #include "version.hpp" @@ -27,6 +28,9 @@ static constexpr off_t BANK_SIZE = 0x4000; // Short options static char const *optstring = "Ccf:hi:jk:L:l:m:n:Oo:p:r:st:VvW:w"; +// Variables for the long-only options +static int longOpt; // `--color` + // Equivalent long options // Please keep in the same order as short opts. // Also, make sure long opts don't create ambiguity: @@ -35,47 +39,54 @@ static char const *optstring = "Ccf:hi:jk:L:l:m:n:Oo:p:r:st:VvW:w"; // This is because long opt matching, even to a single char, is prioritized // over short opt matching. static option const longopts[] = { - {"color-only", no_argument, nullptr, 'C'}, - {"color-compatible", no_argument, nullptr, 'c'}, - {"fix-spec", required_argument, nullptr, 'f'}, - {"help", no_argument, nullptr, 'h'}, - {"game-id", required_argument, nullptr, 'i'}, - {"non-japanese", no_argument, nullptr, 'j'}, - {"new-licensee", required_argument, nullptr, 'k'}, - {"logo", required_argument, nullptr, 'L'}, - {"old-licensee", required_argument, nullptr, 'l'}, - {"mbc-type", required_argument, nullptr, 'm'}, - {"rom-version", required_argument, nullptr, 'n'}, - {"overwrite", no_argument, nullptr, 'O'}, - {"output", required_argument, nullptr, 'o'}, - {"pad-value", required_argument, nullptr, 'p'}, - {"ram-size", required_argument, nullptr, 'r'}, - {"sgb-compatible", no_argument, nullptr, 's'}, - {"title", required_argument, nullptr, 't'}, - {"version", no_argument, nullptr, 'V'}, - {"validate", no_argument, nullptr, 'v'}, - {"warning", required_argument, nullptr, 'W'}, - {nullptr, no_argument, nullptr, 0 } + {"color-only", no_argument, nullptr, 'C'}, + {"color-compatible", no_argument, nullptr, 'c'}, + {"fix-spec", required_argument, nullptr, 'f'}, + {"help", no_argument, nullptr, 'h'}, + {"game-id", required_argument, nullptr, 'i'}, + {"non-japanese", no_argument, nullptr, 'j'}, + {"new-licensee", required_argument, nullptr, 'k'}, + {"logo", required_argument, nullptr, 'L'}, + {"old-licensee", required_argument, nullptr, 'l'}, + {"mbc-type", required_argument, nullptr, 'm'}, + {"rom-version", required_argument, nullptr, 'n'}, + {"overwrite", no_argument, nullptr, 'O'}, + {"output", required_argument, nullptr, 'o'}, + {"pad-value", required_argument, nullptr, 'p'}, + {"ram-size", required_argument, nullptr, 'r'}, + {"sgb-compatible", no_argument, nullptr, 's'}, + {"title", required_argument, nullptr, 't'}, + {"version", no_argument, nullptr, 'V'}, + {"validate", no_argument, nullptr, 'v'}, + {"warning", required_argument, nullptr, 'W'}, + {"color", required_argument, &longOpt, 'c'}, + {nullptr, no_argument, nullptr, 0 }, }; -// clang-format off: long string literal -static Usage usage( - "Usage: rgbfix [-hjOsVvw] [-C | -c] [-f ] [-i ] [-k ]\n" - " [-L ] [-l ] [-m ]\n" - " [-n ] [-p ] [-r ] [-t ]\n" - " [-W warning] ...\n" - "Useful options:\n" - " -m, --mbc-type set the MBC type byte to this value; `-m help'\n" - " or `-m list' prints the accepted values\n" - " -p, --pad-value pad to the next valid size using this value\n" - " -r, --ram-size set the cart RAM size byte to this value\n" - " -o, --output set the output file\n" - " -V, --version print RGBFIX version and exit\n" - " -v, --validate fix the header logo and both checksums (`-f lhg')\n" - " -W, --warning enable or disable warnings\n" - "\n" - "For help, use `man rgbfix' or go to https://rgbds.gbdev.io/docs/\n" -); +// clang-format off: nested initializers +static Usage usage = { + .name = "rgbfix", + .flags = { + "[-hjOsVvw]", "[-C | -c]", "[-f ]", "[-i ]", "[-k ]", + "[-L ]", "[-l ]", "[-m ]", "[-n ]", + "[-p ]", "[-r ]", "[-t ]", "[-W warning]", " ...", + }, + .options = { + { + {"-m", "--mbc-type "}, + { + "set the MBC type byte to this value; `-m help'", + "or `-m list' prints the accepted values", + }, + }, + {{"-p", "--pad-value "}, {"pad to the next valid size using this value"}}, + {{"-r", "--ram-size "}, {"set the cart RAM size byte to this value"}}, + {{"-o", "--output "}, {"set the output file"}}, + {{"-V", "--version"}, {"print RGBFIX version and exit"}}, + {{"-v", "--validate"}, {"fix the header logo and both checksums (`-f lhg')"}}, + {{"-W", "--warning "}, {"enable or disable warnings"}}, + }, +}; // clang-format on static uint8_t tpp1Rev[2]; @@ -817,6 +828,19 @@ int main(int argc, char *argv[]) { warnings.state.warningsEnabled = false; break; + // Long-only options + case 0: + if (longOpt == 'c') { + if (!strcasecmp(musl_optarg, "always")) { + style_Enable(true); + } else if (!strcasecmp(musl_optarg, "never")) { + style_Enable(false); + } else if (strcasecmp(musl_optarg, "auto")) { + fatal("Invalid argument for option '--color'"); + } + } + break; + default: usage.printAndExit(1); // LCOV_EXCL_LINE } diff --git a/src/fix/mbc.cpp b/src/fix/mbc.cpp index 12f95ff6..f937a6f1 100644 --- a/src/fix/mbc.cpp +++ b/src/fix/mbc.cpp @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: MIT + #include "fix/mbc.hpp" #include diff --git a/src/fix/warning.cpp b/src/fix/warning.cpp index 47136706..4e7605df 100644 --- a/src/fix/warning.cpp +++ b/src/fix/warning.cpp @@ -1,5 +1,9 @@ +// SPDX-License-Identifier: MIT + #include "fix/warning.hpp" +#include "style.hpp" + // clang-format off: nested initializers Diagnostics warnings = { .metaWarnings = { @@ -20,6 +24,7 @@ Diagnostics warnings = { uint32_t checkErrors(char const *filename) { if (warnings.nbErrors > 0) { + style_Set(stderr, STYLE_RED, true); fprintf( stderr, "Fixing \"%s\" failed with %" PRIu64 " error%s\n", @@ -27,13 +32,16 @@ uint32_t checkErrors(char const *filename) { warnings.nbErrors, warnings.nbErrors == 1 ? "" : "s" ); + style_Reset(stderr); } return warnings.nbErrors; } void error(char const *fmt, ...) { va_list ap; + style_Set(stderr, STYLE_RED, true); fputs("error: ", stderr); + style_Reset(stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); @@ -44,7 +52,9 @@ void error(char const *fmt, ...) { void fatal(char const *fmt, ...) { va_list ap; + style_Set(stderr, STYLE_RED, true); fputs("FATAL: ", stderr); + style_Reset(stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); @@ -62,7 +72,9 @@ void warning(WarningID id, char const *fmt, ...) { break; case WarningBehavior::ENABLED: + style_Set(stderr, STYLE_YELLOW, true); fprintf(stderr, "warning: [-W%s]\n ", flag); + style_Reset(stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); @@ -70,7 +82,9 @@ void warning(WarningID id, char const *fmt, ...) { break; case WarningBehavior::ERROR: + style_Set(stderr, STYLE_RED, true); fprintf(stderr, "error: [-Werror=%s]\n ", flag); + style_Reset(stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); diff --git a/src/gfx/main.cpp b/src/gfx/main.cpp index d1ffbec1..4f5a7a43 100644 --- a/src/gfx/main.cpp +++ b/src/gfx/main.cpp @@ -18,6 +18,7 @@ #include "extern/getopt.hpp" #include "file.hpp" #include "platform.hpp" +#include "style.hpp" #include "usage.hpp" #include "verbosity.hpp" #include "version.hpp" @@ -44,6 +45,9 @@ static struct LocalOptions { // Short options static char const *optstring = "-Aa:B:b:Cc:d:hi:L:l:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvW:wXx:YZ"; +// Variables for the long-only options +static int longOpt; // `--color` + // Equivalent long options // Please keep in the same order as short opts. // Also, make sure long opts don't create ambiguity: @@ -52,59 +56,62 @@ static char const *optstring = "-Aa:B:b:Cc:d:hi:L:l:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvW: // This is because long opt matching, even to a single char, is prioritized // over short opt matching. static option const longopts[] = { - {"auto-attr-map", no_argument, nullptr, 'A'}, - {"attr-map", required_argument, nullptr, 'a'}, - {"background-color", required_argument, nullptr, 'B'}, - {"base-tiles", required_argument, nullptr, 'b'}, - {"color-curve", no_argument, nullptr, 'C'}, - {"colors", required_argument, nullptr, 'c'}, - {"depth", required_argument, nullptr, 'd'}, - {"help", no_argument, nullptr, 'h'}, - {"input-tileset", required_argument, nullptr, 'i'}, - {"slice", required_argument, nullptr, 'L'}, - {"base-palette", required_argument, nullptr, 'l'}, - {"mirror-tiles", no_argument, nullptr, 'm'}, - {"nb-tiles", required_argument, nullptr, 'N'}, - {"nb-palettes", required_argument, nullptr, 'n'}, - {"group-outputs", no_argument, nullptr, 'O'}, - {"output", required_argument, nullptr, 'o'}, - {"auto-palette", no_argument, nullptr, 'P'}, - {"palette", required_argument, nullptr, 'p'}, - {"auto-palette-map", no_argument, nullptr, 'Q'}, - {"palette-map", required_argument, nullptr, 'q'}, - {"reverse", required_argument, nullptr, 'r'}, - {"palette-size", required_argument, nullptr, 's'}, - {"auto-tilemap", no_argument, nullptr, 'T'}, - {"tilemap", required_argument, nullptr, 't'}, - {"unit-size", required_argument, nullptr, 'U'}, - {"unique-tiles", no_argument, nullptr, 'u'}, - {"version", no_argument, nullptr, 'V'}, - {"verbose", no_argument, nullptr, 'v'}, - {"warning", required_argument, nullptr, 'W'}, - {"mirror-x", no_argument, nullptr, 'X'}, - {"trim-end", required_argument, nullptr, 'x'}, - {"mirror-y", no_argument, nullptr, 'Y'}, - {"columns", no_argument, nullptr, 'Z'}, - {nullptr, no_argument, nullptr, 0 } + {"auto-attr-map", no_argument, nullptr, 'A'}, + {"attr-map", required_argument, nullptr, 'a'}, + {"background-color", required_argument, nullptr, 'B'}, + {"base-tiles", required_argument, nullptr, 'b'}, + {"color-curve", no_argument, nullptr, 'C'}, + {"colors", required_argument, nullptr, 'c'}, + {"depth", required_argument, nullptr, 'd'}, + {"help", no_argument, nullptr, 'h'}, + {"input-tileset", required_argument, nullptr, 'i'}, + {"slice", required_argument, nullptr, 'L'}, + {"base-palette", required_argument, nullptr, 'l'}, + {"mirror-tiles", no_argument, nullptr, 'm'}, + {"nb-tiles", required_argument, nullptr, 'N'}, + {"nb-palettes", required_argument, nullptr, 'n'}, + {"group-outputs", no_argument, nullptr, 'O'}, + {"output", required_argument, nullptr, 'o'}, + {"auto-palette", no_argument, nullptr, 'P'}, + {"palette", required_argument, nullptr, 'p'}, + {"auto-palette-map", no_argument, nullptr, 'Q'}, + {"palette-map", required_argument, nullptr, 'q'}, + {"reverse", required_argument, nullptr, 'r'}, + {"palette-size", required_argument, nullptr, 's'}, + {"auto-tilemap", no_argument, nullptr, 'T'}, + {"tilemap", required_argument, nullptr, 't'}, + {"unit-size", required_argument, nullptr, 'U'}, + {"unique-tiles", no_argument, nullptr, 'u'}, + {"version", no_argument, nullptr, 'V'}, + {"verbose", no_argument, nullptr, 'v'}, + {"warning", required_argument, nullptr, 'W'}, + {"mirror-x", no_argument, nullptr, 'X'}, + {"trim-end", required_argument, nullptr, 'x'}, + {"mirror-y", no_argument, nullptr, 'Y'}, + {"columns", no_argument, nullptr, 'Z'}, + {"color", required_argument, &longOpt, 'c'}, + {nullptr, no_argument, nullptr, 0 }, }; -// clang-format off: long string literal -static Usage usage( - "Usage: rgbgfx [-r stride] [-ChmOuVXYZ] [-v [-v ...]] [-a | -A]\n" - " [-b ] [-c ] [-d ] [-i ]\n" - " [-L ] [-l ] [-N ] [-n ]\n" - " [-o ] [-p | -P] [-q | -Q]\n" - " [-s ] [-t | -T] [-x ] \n" - "Useful options:\n" - " -m, --mirror-tiles optimize out mirrored tiles\n" - " -o, --output output the tile data to this path\n" - " -t, --tilemap output the tile map to this path\n" - " -u, --unique-tiles optimize out identical tiles\n" - " -V, --version print RGBGFX version and exit\n" - " -W, --warning enable or disable warnings\n" - "\n" - "For help, use `man rgbgfx' or go to https://rgbds.gbdev.io/docs/\n" -); +// clang-format off: nested initializers +static Usage usage = { + .name = "rgbgfx", + .flags = { + "[-r stride]", "[-ChmOuVXYZ]", "[-v [-v ...]]", "[-a | -A]", "[-b ]", + "[-c ]", "[-d ]", "[-i ]", "[-L ]", "[-l ]", + "[-N ]", "[-n ]", "[-o ]", "[-p | -P]", + "[-q | -Q]", "[-s ]", "[-t | -T]", "[-x ]", + "", + }, + .options = { + {{"-m", "--mirror-tiles"}, {"optimize out mirrored tiles"}}, + {{"-o", "--output "}, {"output the tile data to this path"}}, + {{"-t", "--tilemap "}, {"output the tile map to this path"}}, + {{"-u", "--unique-tiles"}, {"optimize out identical tiles"}}, + {{"-V", "--version"}, {"print RGBGFX version and exit"}}, + {{"-W", "--warning "}, {"enable or disable warnings"}}, + }, +}; // clang-format on // Parses a number at the beginning of a string, moving the pointer to skip the parsed characters. @@ -552,6 +559,17 @@ static char *parseArgv(int argc, char *argv[]) { case 'Z': options.columnMajor = true; break; + case 0: // Long-only options + if (longOpt == 'c') { + if (!strcasecmp(musl_optarg, "always")) { + style_Enable(true); + } else if (!strcasecmp(musl_optarg, "never")) { + style_Enable(false); + } else if (strcasecmp(musl_optarg, "auto")) { + fatal("Invalid argument for option '--color'"); + } + } + break; case 1: // Positional argument, requested by leading `-` in opt string if (musl_optarg[0] == '@') { // Instruct the caller to process that at-file diff --git a/src/gfx/process.cpp b/src/gfx/process.cpp index df0f4668..52bef10a 100644 --- a/src/gfx/process.cpp +++ b/src/gfx/process.cpp @@ -630,7 +630,7 @@ static void outputUnoptimizedTileData( uint64_t nbKeptTiles = nbTiles > options.trim ? nbTiles - options.trim : 0; uint64_t tileIdx = 0; - for (auto [tile, attr] : zip(image.visitAsTiles(), attrmap)) { + for (auto const &[tile, attr] : zip(image.visitAsTiles(), attrmap)) { // Do not emit fully-background tiles. if (attr.isBackgroundTile()) { ++tileIdx; @@ -799,7 +799,7 @@ static UniqueTiles dedupTiles( } bool inputWithoutOutput = !options.inputTileset.empty() && options.output.empty(); - for (auto [tile, attr] : zip(image.visitAsTiles(), attrmap)) { + for (auto const &[tile, attr] : zip(image.visitAsTiles(), attrmap)) { if (attr.isBackgroundTile()) { attr.xFlip = false; attr.yFlip = false; diff --git a/src/gfx/warning.cpp b/src/gfx/warning.cpp index 33ea9dc0..01e4016d 100644 --- a/src/gfx/warning.cpp +++ b/src/gfx/warning.cpp @@ -7,6 +7,8 @@ #include #include +#include "style.hpp" + // clang-format off: nested initializers Diagnostics warnings = { .metaWarnings = { @@ -25,12 +27,14 @@ Diagnostics warnings = { [[noreturn]] void giveUp() { + style_Set(stderr, STYLE_RED, true); fprintf( stderr, "Conversion aborted after %" PRIu64 " error%s\n", warnings.nbErrors, warnings.nbErrors == 1 ? "" : "s" ); + style_Reset(stderr); exit(1); } @@ -42,7 +46,9 @@ void requireZeroErrors() { void error(char const *fmt, ...) { va_list ap; + style_Set(stderr, STYLE_RED, true); fputs("error: ", stderr); + style_Reset(stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); @@ -54,7 +60,9 @@ void error(char const *fmt, ...) { [[noreturn]] void fatal(char const *fmt, ...) { va_list ap; + style_Set(stderr, STYLE_RED, true); fputs("FATAL: ", stderr); + style_Reset(stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); @@ -73,7 +81,9 @@ void warning(WarningID id, char const *fmt, ...) { break; case WarningBehavior::ENABLED: + style_Set(stderr, STYLE_YELLOW, true); fprintf(stderr, "warning: [-W%s]\n ", flag); + style_Reset(stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); @@ -81,7 +91,9 @@ void warning(WarningID id, char const *fmt, ...) { break; case WarningBehavior::ERROR: + style_Set(stderr, STYLE_RED, true); fprintf(stderr, "error: [-Werror=%s]\n ", flag); + style_Reset(stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); diff --git a/src/link/main.cpp b/src/link/main.cpp index bceb34d9..0b89388a 100644 --- a/src/link/main.cpp +++ b/src/link/main.cpp @@ -16,6 +16,7 @@ #include "itertools.hpp" #include "platform.hpp" #include "script.hpp" // Generated from script.y +#include "style.hpp" #include "usage.hpp" #include "util.hpp" // UpperMap, printChar #include "verbosity.hpp" @@ -37,6 +38,9 @@ static char const *linkerScriptName = nullptr; // -l // Short options static char const *optstring = "dhl:m:Mn:O:o:p:S:tVvW:wx"; +// Variables for the long-only options +static int longOpt; // `--color` + // Equivalent long options // Please keep in the same order as short opts. // Also, make sure long opts don't create ambiguity: @@ -45,42 +49,44 @@ static char const *optstring = "dhl:m:Mn:O:o:p:S:tVvW:wx"; // This is because long opt matching, even to a single char, is prioritized // over short opt matching. static option const longopts[] = { - {"dmg", no_argument, nullptr, 'd'}, - {"help", no_argument, nullptr, 'h'}, - {"linkerscript", required_argument, nullptr, 'l'}, - {"map", required_argument, nullptr, 'm'}, - {"no-sym-in-map", no_argument, nullptr, 'M'}, - {"sym", required_argument, nullptr, 'n'}, - {"overlay", required_argument, nullptr, 'O'}, - {"output", required_argument, nullptr, 'o'}, - {"pad", required_argument, nullptr, 'p'}, - {"scramble", required_argument, nullptr, 'S'}, - {"tiny", no_argument, nullptr, 't'}, - {"version", no_argument, nullptr, 'V'}, - {"verbose", no_argument, nullptr, 'v'}, - {"warning", required_argument, nullptr, 'W'}, - {"wramx", no_argument, nullptr, 'w'}, - {"nopad", no_argument, nullptr, 'x'}, - {nullptr, no_argument, nullptr, 0 } + {"dmg", no_argument, nullptr, 'd'}, + {"help", no_argument, nullptr, 'h'}, + {"linkerscript", required_argument, nullptr, 'l'}, + {"map", required_argument, nullptr, 'm'}, + {"no-sym-in-map", no_argument, nullptr, 'M'}, + {"sym", required_argument, nullptr, 'n'}, + {"overlay", required_argument, nullptr, 'O'}, + {"output", required_argument, nullptr, 'o'}, + {"pad", required_argument, nullptr, 'p'}, + {"scramble", required_argument, nullptr, 'S'}, + {"tiny", no_argument, nullptr, 't'}, + {"version", no_argument, nullptr, 'V'}, + {"verbose", no_argument, nullptr, 'v'}, + {"warning", required_argument, nullptr, 'W'}, + {"wramx", no_argument, nullptr, 'w'}, + {"nopad", no_argument, nullptr, 'x'}, + {"color", required_argument, &longOpt, 'c'}, + {nullptr, no_argument, nullptr, 0 }, }; -// clang-format off: long string literal -static Usage usage( - "Usage: rgblink [-dhMtVvwx] [-l script] [-m map_file] [-n sym_file]\n" - " [-O overlay_file] [-o out_file] [-p pad_value]\n" - " [-S spec] ...\n" - "Useful options:\n" - " -l, --linkerscript set the input linker script\n" - " -m, --map set the output map file\n" - " -n, --sym set the output symbol list file\n" - " -o, --output set the output file\n" - " -p, --pad set the value to pad between sections with\n" - " -x, --nopad disable padding of output binary\n" - " -V, --version print RGBLINK version and exit\n" - " -W, --warning enable or disable warnings\n" - "\n" - "For help, use `man rgblink' or go to https://rgbds.gbdev.io/docs/\n" -); +// clang-format off: nested initializers +static Usage usage = { + .name = "rgblink", + .flags = { + "[-dhMtVvwx]", "[-l script]", "[-m map_file]", "[-n sym_file]", "[-O overlay_file]", + "[-o out_file]", "[-p pad_value]", "[-S spec]", " ...", + }, + .options = { + {{"-l", "--linkerscript "}, {"set the input linker script"}}, + {{"-m", "--map "}, {"set the output map file"}}, + {{"-n", "--sym "}, {"set the output symbol list file"}}, + {{"-o", "--output "}, {"set the output file"}}, + {{"-p", "--pad "}, {"set the value to pad between sections with"}}, + {{"-x", "--nopad"}, {"disable padding of output binary"}}, + {{"-V", "--version"}, {"print RGBLINK version and exit"}}, + {{"-W", "--warning "}, {"enable or disable warnings"}}, + }, +}; // clang-format on // LCOV_EXCL_START @@ -174,21 +180,29 @@ std::string const &FileStackNode::dump(uint32_t curLineNo) const { if (std::holds_alternative>(data)) { assume(parent); // REPT nodes use their parent's name std::string const &lastName = parent->dump(lineNo); + style_Set(stderr, STYLE_CYAN, false); fputs(" -> ", stderr); + style_Set(stderr, STYLE_CYAN, true); fputs(lastName.c_str(), stderr); for (uint32_t iter : iters()) { fprintf(stderr, "::REPT~%" PRIu32, iter); } + style_Set(stderr, STYLE_CYAN, false); fprintf(stderr, "(%" PRIu32 ")", curLineNo); + style_Reset(stderr); return lastName; } else { if (parent) { parent->dump(lineNo); + style_Set(stderr, STYLE_CYAN, false); fputs(" -> ", stderr); } std::string const &nodeName = name(); + style_Set(stderr, STYLE_CYAN, true); fputs(nodeName.c_str(), stderr); + style_Set(stderr, STYLE_CYAN, false); fprintf(stderr, "(%" PRIu32 ")", curLineNo); + style_Reset(stderr); return nodeName; } } @@ -388,6 +402,17 @@ int main(int argc, char *argv[]) { // implies tiny mode options.is32kMode = true; break; + case 0: // Long-only options + if (longOpt == 'c') { + if (!strcasecmp(musl_optarg, "always")) { + style_Enable(true); + } else if (!strcasecmp(musl_optarg, "never")) { + style_Enable(false); + } else if (strcasecmp(musl_optarg, "auto")) { + fatal("Invalid argument for option '--color'"); + } + } + break; default: usage.printAndExit(1); // LCOV_EXCL_LINE } diff --git a/src/link/warning.cpp b/src/link/warning.cpp index 7c041281..8e682636 100644 --- a/src/link/warning.cpp +++ b/src/link/warning.cpp @@ -5,6 +5,8 @@ #include #include +#include "style.hpp" + #include "link/main.hpp" // clang-format off: nested initializers @@ -33,24 +35,29 @@ static void printDiag( char const *fmt, va_list args, char const *type, + StyleColor color, char const *flagfmt, char const *flag ) { + style_Set(stderr, color, true); fprintf(stderr, "%s: ", type); if (src) { src->dump(lineNo); fputs(": ", stderr); } if (flagfmt) { + style_Set(stderr, color, true); fprintf(stderr, flagfmt, flag); fputs("\n ", stderr); } + style_Reset(stderr); vfprintf(stderr, fmt, args); putc('\n', stderr); } [[noreturn]] static void abortLinking(char const *verb) { + style_Set(stderr, STYLE_RED, true); fprintf( stderr, "Linking %s with %" PRIu64 " error%s\n", @@ -58,27 +65,28 @@ static void abortLinking(char const *verb) { warnings.nbErrors, warnings.nbErrors == 1 ? "" : "s" ); + style_Reset(stderr); exit(1); } void warning(FileStackNode const *src, uint32_t lineNo, char const *fmt, ...) { va_list args; va_start(args, fmt); - printDiag(src, lineNo, fmt, args, "warning", nullptr, 0); + printDiag(src, lineNo, fmt, args, "warning", STYLE_YELLOW, nullptr, nullptr); va_end(args); } void warning(char const *fmt, ...) { va_list args; va_start(args, fmt); - printDiag(nullptr, 0, fmt, args, "warning", nullptr, 0); + printDiag(nullptr, 0, fmt, args, "warning", STYLE_YELLOW, nullptr, nullptr); va_end(args); } void error(FileStackNode const *src, uint32_t lineNo, char const *fmt, ...) { va_list args; va_start(args, fmt); - printDiag(src, lineNo, fmt, args, "error", nullptr, 0); + printDiag(src, lineNo, fmt, args, "error", STYLE_RED, nullptr, nullptr); va_end(args); warnings.incrementErrors(); @@ -87,7 +95,7 @@ void error(FileStackNode const *src, uint32_t lineNo, char const *fmt, ...) { void error(char const *fmt, ...) { va_list args; va_start(args, fmt); - printDiag(nullptr, 0, fmt, args, "error", nullptr, 0); + printDiag(nullptr, 0, fmt, args, "error", STYLE_RED, nullptr, nullptr); va_end(args); warnings.incrementErrors(); @@ -95,7 +103,9 @@ void error(char const *fmt, ...) { void errorNoDump(char const *fmt, ...) { va_list args; + style_Set(stderr, STYLE_RED, true); fputs("error: ", stderr); + style_Reset(stderr); va_start(args, fmt); vfprintf(stderr, fmt, args); va_end(args); @@ -104,7 +114,13 @@ void errorNoDump(char const *fmt, ...) { } void scriptError(char const *name, uint32_t lineNo, char const *fmt, va_list args) { - fprintf(stderr, "error: %s(%" PRIu32 "): ", name, lineNo); + style_Set(stderr, STYLE_RED, true); + fputs("error: ", stderr); + style_Set(stderr, STYLE_CYAN, true); + fputs(name, stderr); + style_Set(stderr, STYLE_CYAN, false); + fprintf(stderr, "(%" PRIu32 "): ", lineNo); + style_Reset(stderr); vfprintf(stderr, fmt, args); putc('\n', stderr); @@ -115,7 +131,7 @@ void scriptError(char const *name, uint32_t lineNo, char const *fmt, va_list arg void fatal(FileStackNode const *src, uint32_t lineNo, char const *fmt, ...) { va_list args; va_start(args, fmt); - printDiag(src, lineNo, fmt, args, "FATAL", nullptr, 0); + printDiag(src, lineNo, fmt, args, "FATAL", STYLE_RED, nullptr, nullptr); va_end(args); warnings.incrementErrors(); @@ -126,7 +142,7 @@ void fatal(FileStackNode const *src, uint32_t lineNo, char const *fmt, ...) { void fatal(char const *fmt, ...) { va_list args; va_start(args, fmt); - printDiag(nullptr, 0, fmt, args, "FATAL", nullptr, 0); + printDiag(nullptr, 0, fmt, args, "FATAL", STYLE_RED, nullptr, nullptr); va_end(args); warnings.incrementErrors(); @@ -150,11 +166,11 @@ void warning(FileStackNode const *src, uint32_t lineNo, WarningID id, char const break; case WarningBehavior::ENABLED: - printDiag(src, lineNo, fmt, args, "warning", "[-W%s]", flag); + printDiag(src, lineNo, fmt, args, "warning", STYLE_RED, "[-W%s]", flag); break; case WarningBehavior::ERROR: - printDiag(src, lineNo, fmt, args, "error", "[-Werror=%s]", flag); + printDiag(src, lineNo, fmt, args, "error", STYLE_YELLOW, "[-Werror=%s]", flag); warnings.incrementErrors(); break; diff --git a/src/linkdefs.cpp b/src/linkdefs.cpp index 7d837896..d1b14804 100644 --- a/src/linkdefs.cpp +++ b/src/linkdefs.cpp @@ -14,7 +14,7 @@ SectionTypeInfo sectionTypeInfo[SECTTYPE_INVALID] = { .size = 0x2000, // Patched to 0x1000 if !isWRAM0Mode .firstBank = 0, .lastBank = 0, - }, + }, { .name = "VRAM"s, .startAddr = 0x8000, @@ -28,42 +28,42 @@ SectionTypeInfo sectionTypeInfo[SECTTYPE_INVALID] = { .size = 0x4000, .firstBank = 1, .lastBank = 65535, - }, + }, { .name = "ROM0"s, .startAddr = 0x0000, .size = 0x8000, // Patched to 0x4000 if !is32kMode .firstBank = 0, .lastBank = 0, - }, + }, { .name = "HRAM"s, .startAddr = 0xFF80, .size = 0x007F, .firstBank = 0, .lastBank = 0, - }, + }, { .name = "WRAMX"s, .startAddr = 0xD000, .size = 0x1000, .firstBank = 1, .lastBank = 7, - }, + }, { .name = "SRAM"s, .startAddr = 0xA000, .size = 0x2000, .firstBank = 0, .lastBank = 255, - }, + }, { .name = "OAM"s, .startAddr = 0xFE00, .size = 0x00A0, .firstBank = 0, .lastBank = 0, - }, + }, }; // clang-format on diff --git a/src/style.cpp b/src/style.cpp new file mode 100644 index 00000000..9a088756 --- /dev/null +++ b/src/style.cpp @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT + +// This implementation was based on https://github.com/agauniyal/rang/ +// and adapted for RGBDS. + +#include "style.hpp" + +#include // getenv +#include + +#include "platform.hpp" // isatty + +#if !STYLE_ANSI +// clang-format off: maintain `include` order + #define WIN32_LEAN_AND_MEAN // Include less from `windows.h` + #include +// clang-format on +#endif + +enum Tribool { TRI_NO, TRI_YES, TRI_MAYBE }; + +#if !STYLE_ANSI +static const HANDLE outHandle = GetStdHandle(STD_OUTPUT_HANDLE); +static const HANDLE errHandle = GetStdHandle(STD_ERROR_HANDLE); + +static const WORD defaultAttrib = []() { + if (CONSOLE_SCREEN_BUFFER_INFO info; GetConsoleScreenBufferInfo(outHandle, &info) + || GetConsoleScreenBufferInfo(errHandle, &info)) { + return info.wAttributes; + } + return static_cast((STYLE_BLACK << 4) | (STYLE_GRAY | 8)); +}(); + +static HANDLE getHandle(FILE *file) { + return file == stdout ? outHandle : file == stderr ? errHandle : INVALID_HANDLE_VALUE; +} +#endif // !STYLE_ANSI + +static Tribool forceStyle = []() { + if (char const *forceColor = getenv("FORCE_COLOR"); + forceColor && strcmp(forceColor, "") && strcmp(forceColor, "0")) { + return TRI_YES; + } + if (char const *noColor = getenv("NO_COLOR"); + noColor && strcmp(noColor, "") && strcmp(noColor, "0")) { + return TRI_NO; + } + return TRI_MAYBE; +}(); + +static bool isTerminal(FILE *file) { + static bool isOutTerminal = isatty(STDOUT_FILENO); + static bool isErrTerminal = isatty(STDERR_FILENO); + + return (file == stdout && isOutTerminal) || (file == stderr && isErrTerminal); +} + +static bool allowStyle(FILE *file) { + return forceStyle == TRI_YES || (forceStyle == TRI_MAYBE && isTerminal(file)); +} + +void style_Enable(bool enable) { + forceStyle = enable ? TRI_YES : TRI_NO; +} + +void style_Set(FILE *file, StyleColor color, bool bold) { + if (!allowStyle(file)) { + return; + } + + // LCOV_EXCL_START +#if STYLE_ANSI + fprintf(file, "\033[%dm", static_cast(color) + (bold ? 90 : 30)); +#else + if (HANDLE handle = getHandle(file); handle != INVALID_HANDLE_VALUE) { + fflush(file); + SetConsoleTextAttribute(handle, (defaultAttrib & ~0xF) | (color | (bold ? 8 : 0))); + } +#endif + // LCOV_EXCL_STOP +} + +void style_Reset(FILE *file) { + if (!allowStyle(file)) { + return; + } + + // LCOV_EXCL_START +#if STYLE_ANSI + fputs("\033[m", file); +#else + if (HANDLE handle = getHandle(file); handle != INVALID_HANDLE_VALUE) { + fflush(file); + SetConsoleTextAttribute(handle, defaultAttrib); + } +#endif + // LCOV_EXCL_STOP +} diff --git a/src/usage.cpp b/src/usage.cpp index b0e068a3..66d97872 100644 --- a/src/usage.cpp +++ b/src/usage.cpp @@ -2,17 +2,111 @@ #include "usage.hpp" +#include #include #include +#include "helpers.hpp" +#include "style.hpp" + +static constexpr size_t maxLineLen = 79; + +// LCOV_EXCL_START + void Usage::printAndExit(int code) const { - fputs(usage, stderr); + FILE *file = code ? stderr : stdout; + + // Print "Usage: " + style_Set(file, STYLE_GREEN, true); + fputs("Usage: ", file); + style_Set(file, STYLE_CYAN, true); + fputs(name.c_str(), file); + size_t padFlags = literal_strlen("Usage: ") + name.length(); + + // Print the flags after the program name, indented to the same level + style_Set(file, STYLE_CYAN, false); + size_t flagsWidth = padFlags; + for (std::string const &flag : flags) { + if (flagsWidth + 1 + flag.length() > maxLineLen) { + fprintf(file, "\n%*c", static_cast(padFlags), ' '); + flagsWidth = padFlags; + } + fprintf(file, " %s", flag.c_str()); + flagsWidth += 1 + flag.length(); + } + fputs("\n\n", file); + + // Measure the options' flags + size_t padOpts = 0; + for (auto const &item : options) { + size_t pad = 0; + for (size_t i = 0; i < item.first.size(); ++i) { + if (i > 0) { + pad += literal_strlen(", "); + } + pad += item.first[i].length(); + } + if (pad > padOpts) { + padOpts = pad; + } + } + int optIndent = static_cast(literal_strlen(" ") + padOpts); + + // Print the options + if (!options.empty()) { + style_Set(file, STYLE_GREEN, true); + fputs("Useful options:\n", file); + } + for (auto const &[opts, description] : options) { + fputs(" ", file); + + // Print the comma-separated options + size_t optWidth = 0; + for (size_t i = 0; i < opts.size(); ++i) { + if (i > 0) { + style_Reset(file); + fputs(", ", file); + optWidth += literal_strlen(", "); + } + style_Set(file, STYLE_CYAN, false); + fputs(opts[i].c_str(), file); + optWidth += opts[i].length(); + } + if (optWidth < padOpts) { + fprintf(file, "%*c", static_cast(padOpts - optWidth), ' '); + } + + // Print the description lines, indented to the same level + for (size_t i = 0; i < description.size(); ++i) { + style_Reset(file); + if (i > 0) { + fprintf(file, "\n%*c", optIndent, ' '); + } + fprintf(file, " %s", description[i].c_str()); + } + putc('\n', file); + } + + // Print the link for further help information + style_Reset(file); + fputs("\nFor more help, use `", file); + style_Set(file, STYLE_CYAN, true); + fprintf(file, "man %s", name.c_str()); + style_Reset(file); + fputs("' or go to ", file); + style_Set(file, STYLE_BLUE, true); + fputs("https://rgbds.gbdev.io/docs/", file); + style_Reset(file); + putc('\n', file); + exit(code); } void Usage::printAndExit(char const *fmt, ...) const { va_list args; + style_Set(stderr, STYLE_RED, true); fputs("FATAL: ", stderr); + style_Reset(stderr); va_start(args, fmt); vfprintf(stderr, fmt, args); va_end(args); @@ -20,3 +114,5 @@ void Usage::printAndExit(char const *fmt, ...) const { printAndExit(1); } + +// LCOV_EXCL_STOP diff --git a/src/verbosity.cpp b/src/verbosity.cpp index 0ac97604..79b5e2c1 100644 --- a/src/verbosity.cpp +++ b/src/verbosity.cpp @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: MIT + #include "verbosity.hpp" #include @@ -8,16 +10,18 @@ static Verbosity verbosity = VERB_NONE; +bool checkVerbosity(Verbosity level) { + return verbosity >= level; +} + +// LCOV_EXCL_START + void incrementVerbosity() { if (verbosity < VERB_VVVVVV) { verbosity = static_cast(verbosity + 1); } } -bool checkVerbosity(Verbosity level) { - return verbosity >= level; -} - void printVVVVVVerbosity() { if (!checkVerbosity(VERB_VVVVVV)) { return; @@ -67,3 +71,5 @@ void printVVVVVVerbosity() { } putc('\n', stderr); } + +// LCOV_EXCL_STOP