From 845b297aa490fdd963962f2578da4f2424214b75 Mon Sep 17 00:00:00 2001 From: "Derek T. Jones" Date: Mon, 13 Apr 2026 23:04:37 -0700 Subject: [PATCH 1/3] Write the end-of-program banner to stderr, not stdout --- src/main.cc | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main.cc b/src/main.cc index 2632ec1..bebcab7 100644 --- a/src/main.cc +++ b/src/main.cc @@ -58,11 +58,7 @@ namespace archetype { ~Session() { if (not silent_) { - UserOutput output = Universe::instance().output(); - output->endLine(); - output->put("Archetype "); - output->put(VersionString); - output->endLine(); + std::cerr << "Archetype " << VersionString << std::endl; } TestRegistry::destroy(); Universe::destroy(); From 37fa208cafff771301408f48f2afece29672a8ab Mon Sep 17 00:00:00 2001 From: "Derek T. Jones" Date: Mon, 13 Apr 2026 23:29:59 -0700 Subject: [PATCH 2/3] Add write_centered statement for terminal-width-aware centering Introduces a new output keyword, write_centered, that centers its concatenated arguments against the current terminal width, replacing hand-eyeballed leading spaces that assumed an 80-column screen. Adds IUserOutput::center() with a default implementation and a WrappedOutput override that pads against maxColumns_. Updates the starship.arch and gorreven.arch title blocks to use the new statement. --- games/gorreven.arch | 14 +++++++------- games/starship.arch | 14 +++++++------- src/Keywords.cc | 1 + src/Keywords.hh | 1 + src/Statement.cc | 12 ++++++++++-- src/TestWrappedOutput.cc | 28 ++++++++++++++++++++++++++++ src/TestWrappedOutput.hh | 1 + src/UserOutput.hh | 1 + src/WrappedOutput.cc | 12 ++++++++++++ src/WrappedOutput.hh | 1 + 10 files changed, 69 insertions(+), 16 deletions(-) diff --git a/games/gorreven.arch b/games/gorreven.arch index b13ba59..e6a18b0 100644 --- a/games/gorreven.arch +++ b/games/gorreven.arch @@ -122,13 +122,13 @@ methods >>my life won't be worth a fake dollar bill. None of our lives will. >> >> ->> Derek Jones Presents... ->> ->> an Archetype Adventure ->> ->> "The Gorreven Papers" ->> ->> + write_centered "Derek Jones Presents..." + write + write_centered "an Archetype Adventure" + write + write_centered "\"The Gorreven Papers\"" + write + write } # FIRSTDESC 'LONGDESC' : diff --git a/games/starship.arch b/games/starship.arch index 221b892..1b4e2b7 100644 --- a/games/starship.arch +++ b/games/starship.arch @@ -96,13 +96,13 @@ methods >> >>Who am I...? >> ->> Derek Jones Presents ->> ->> an Archetype Adventure ->> ->> STARSHIP SOLITAIRE ->> ->> + write_centered "Derek Jones Presents" + write + write_centered "an Archetype Adventure" + write + write_centered "STARSHIP SOLITAIRE" + write + write } # FIRSTDESC 'LONGDESC' : { diff --git a/src/Keywords.cc b/src/Keywords.cc index 0fb0474..759dcae 100644 --- a/src/Keywords.cc +++ b/src/Keywords.cc @@ -68,6 +68,7 @@ namespace archetype { RESERVE(RW_WHILE, "while"); RESERVE(RW_WRITE, "write"); RESERVE(RW_WRITES, "writes"); + RESERVE(RW_WRITE_CENTERED, "write_centered"); OPERATOR(OP_PAIR, "@"); OPERATOR(OP_CONCAT, "&"); diff --git a/src/Keywords.hh b/src/Keywords.hh index e89ac75..bb3f94c 100644 --- a/src/Keywords.hh +++ b/src/Keywords.hh @@ -52,6 +52,7 @@ namespace archetype { RW_WHILE, RW_WRITE, RW_WRITES, + RW_WRITE_CENTERED, RW_WRITE_PARAGRAPH, NumReserved }; diff --git a/src/Statement.cc b/src/Statement.cc index b46cd22..7fe2226 100644 --- a/src/Statement.cc +++ b/src/Statement.cc @@ -477,6 +477,7 @@ namespace archetype { Value OutputStatement::execute() const { Value last_value(new UndefinedValue); + ostringstream centered; for (auto const& expr : expressions_) { last_value = expr->evaluate(); if (writeType_ == Keywords::RW_DISPLAY) { @@ -488,10 +489,16 @@ namespace archetype { } Value v_s = last_value->stringConversion(); if (v_s->isDefined()) { - Universe::instance().output()->put(v_s->getString()); + if (writeType_ == Keywords::RW_WRITE_CENTERED) { + centered << v_s->getString(); + } else { + Universe::instance().output()->put(v_s->getString()); + } } } - if (writeType_ != Keywords::RW_WRITES) { + if (writeType_ == Keywords::RW_WRITE_CENTERED) { + Universe::instance().output()->center(centered.str()); + } else if (writeType_ != Keywords::RW_WRITES) { Universe::instance().output()->endLine(); } if (writeType_ == Keywords::RW_STOP) { @@ -766,6 +773,7 @@ namespace archetype { case Keywords::RW_DISPLAY: case Keywords::RW_WRITE: case Keywords::RW_WRITES: + case Keywords::RW_WRITE_CENTERED: case Keywords::RW_STOP: the_stmt.reset(new OutputStatement(word)); break; diff --git a/src/TestWrappedOutput.cc b/src/TestWrappedOutput.cc index babe35b..23852c4 100644 --- a/src/TestWrappedOutput.cc +++ b/src/TestWrappedOutput.cc @@ -50,7 +50,35 @@ namespace archetype { out() << "TestWrappedOutput finished." << endl; } + void TestWrappedOutput::testCenter_() { + UserOutput user_soutput{new StringOutput}; + StringOutput& strout(*dynamic_cast(user_soutput.get())); + UserOutput user_output{new WrappedOutput{user_soutput}}; + WrappedOutput& wrout(*dynamic_cast(user_output.get())); + wrout.setMaxColumns(20); + string title = "Hello"; + user_output->center(title); + string result = strout.getOutput(); + // (20 - 5) / 2 = 7 leading spaces + ARCHETYPE_TEST_EQUAL(result, string(" Hello\n")); + + // A line equal to or wider than the column count gets no padding. + StringOutput& strout2 = strout; + wrout.setMaxColumns(5); + user_output->center("Hello, world!"); + string wide = strout2.getOutput(); + ARCHETYPE_TEST(wide.find("Hello, world!") != string::npos); + + // Zero columns means indeterminate width: emit unpadded. + wrout.setMaxColumns(0); + user_output->center("X"); + ARCHETYPE_TEST(strout2.getOutput().find("\nX\n") != string::npos + or strout2.getOutput().rfind("X\n") == strout2.getOutput().size() - 2); + out() << "TestWrappedOutput::testCenter_ finished." << endl; + } + void TestWrappedOutput::runTests_() { testBasicWrap_(); + testCenter_(); } } diff --git a/src/TestWrappedOutput.hh b/src/TestWrappedOutput.hh index a176d48..e257c7b 100644 --- a/src/TestWrappedOutput.hh +++ b/src/TestWrappedOutput.hh @@ -14,6 +14,7 @@ namespace archetype { class TestWrappedOutput : public ITestSuite { void testBasicWrap_(); + void testCenter_(); protected: virtual void runTests_() override; public: diff --git a/src/UserOutput.hh b/src/UserOutput.hh index 806d3a8..00b2f75 100644 --- a/src/UserOutput.hh +++ b/src/UserOutput.hh @@ -24,6 +24,7 @@ namespace archetype { virtual void endLine() = 0; virtual void resetPager() { } virtual void banner(char ch) = 0; + virtual void center(const std::string& line) { put(line); endLine(); } }; typedef std::shared_ptr UserOutput; } diff --git a/src/WrappedOutput.cc b/src/WrappedOutput.cc index 4a79fde..fe58f0d 100644 --- a/src/WrappedOutput.cc +++ b/src/WrappedOutput.cc @@ -80,6 +80,18 @@ namespace archetype { resetCursor(); } + void WrappedOutput::center(const std::string& line) { + if (cursor_ != 0) { + endLine(); + } + if (maxColumns_ > 0 and int(line.size()) < maxColumns_) { + int pad = (maxColumns_ - int(line.size())) / 2; + output_->put(string(pad, ' ')); + } + output_->put(line); + endLine(); + } + void WrappedOutput::banner(char ch) { if (cursor_ != 0) { endLine(); diff --git a/src/WrappedOutput.hh b/src/WrappedOutput.hh index 1e97834..dc59da8 100644 --- a/src/WrappedOutput.hh +++ b/src/WrappedOutput.hh @@ -23,6 +23,7 @@ namespace archetype { virtual void put(const std::string& line) override; virtual void endLine() override; virtual void banner(char ch) override; + virtual void center(const std::string& line) override; // Get and set the columns of text available in the console. // Zero is a special value meaning that the columns are From dfb95394c87bdf9b2d115900a70e3e38692f406d Mon Sep 17 00:00:00 2001 From: "Derek T. Jones" Date: Sat, 18 Apr 2026 16:53:29 -0700 Subject: [PATCH 3/3] Tighten write_centered tests and lazy-allocate the centering buffer Addresses review feedback on PR #18: - TestWrappedOutput::testCenter_ now asserts on output deltas rather than find() or rfind() across cumulative output, and pins the exact-width edge case. - OutputStatement::execute only constructs the ostringstream on the RW_WRITE_CENTERED path; its truthiness doubles as the write-mode flag elsewhere in the function. - TestStatement gains an end-to-end case that exercises keyword reservation, parsing, argument concatenation, and routing to IUserOutput::center(). Co-Authored-By: Claude Opus 4.7 --- src/Statement.cc | 13 ++++++++----- src/TestStatement.cc | 8 ++++++++ src/TestWrappedOutput.cc | 29 +++++++++++++++++++---------- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/Statement.cc b/src/Statement.cc index 7fe2226..68ddd88 100644 --- a/src/Statement.cc +++ b/src/Statement.cc @@ -477,7 +477,10 @@ namespace archetype { Value OutputStatement::execute() const { Value last_value(new UndefinedValue); - ostringstream centered; + unique_ptr centered; + if (writeType_ == Keywords::RW_WRITE_CENTERED) { + centered.reset(new ostringstream); + } for (auto const& expr : expressions_) { last_value = expr->evaluate(); if (writeType_ == Keywords::RW_DISPLAY) { @@ -489,15 +492,15 @@ namespace archetype { } Value v_s = last_value->stringConversion(); if (v_s->isDefined()) { - if (writeType_ == Keywords::RW_WRITE_CENTERED) { - centered << v_s->getString(); + if (centered) { + *centered << v_s->getString(); } else { Universe::instance().output()->put(v_s->getString()); } } } - if (writeType_ == Keywords::RW_WRITE_CENTERED) { - Universe::instance().output()->center(centered.str()); + if (centered) { + Universe::instance().output()->center(centered->str()); } else if (writeType_ != Keywords::RW_WRITES) { Universe::instance().output()->endLine(); } diff --git a/src/TestStatement.cc b/src/TestStatement.cc index b049129..6df6310 100644 --- a/src/TestStatement.cc +++ b/src/TestStatement.cc @@ -87,6 +87,14 @@ namespace archetype { string expected_4_2 = "Hello, world!\n"; ARCHETYPE_TEST_EQUAL(actual_4_2, expected_4_2); + // Capture uses a bare StringOutput, so center() falls through to the + // default put+endLine; padding math is covered in TestWrappedOutput. + Statement stmt4c = make_stmt_from_str("write_centered \"Hello, \", \"world!\""); + ARCHETYPE_TEST(stmt4c != nullptr); + Capture capture4c; + stmt4c->execute(); + ARCHETYPE_TEST_EQUAL(capture4c.getCapture(), string("Hello, world!\n")); + Statement stmt5 = make_stmt_from_str(">>Now, this is a \"quote\"."); ARCHETYPE_TEST(stmt5 != nullptr); Capture capture5; diff --git a/src/TestWrappedOutput.cc b/src/TestWrappedOutput.cc index 23852c4..bd65e71 100644 --- a/src/TestWrappedOutput.cc +++ b/src/TestWrappedOutput.cc @@ -55,25 +55,34 @@ namespace archetype { StringOutput& strout(*dynamic_cast(user_soutput.get())); UserOutput user_output{new WrappedOutput{user_soutput}}; WrappedOutput& wrout(*dynamic_cast(user_output.get())); + + auto delta = [&](size_t& mark) { + string all = strout.getOutput(); + string d = all.substr(mark); + mark = all.size(); + return d; + }; + size_t mark = 0; + wrout.setMaxColumns(20); - string title = "Hello"; - user_output->center(title); - string result = strout.getOutput(); + user_output->center("Hello"); // (20 - 5) / 2 = 7 leading spaces - ARCHETYPE_TEST_EQUAL(result, string(" Hello\n")); + ARCHETYPE_TEST_EQUAL(delta(mark), string(" Hello\n")); - // A line equal to or wider than the column count gets no padding. - StringOutput& strout2 = strout; + // A line at the column count gets no padding. wrout.setMaxColumns(5); + user_output->center("Hello"); + ARCHETYPE_TEST_EQUAL(delta(mark), string("Hello\n")); + + // A line wider than the column count gets no padding. user_output->center("Hello, world!"); - string wide = strout2.getOutput(); - ARCHETYPE_TEST(wide.find("Hello, world!") != string::npos); + ARCHETYPE_TEST_EQUAL(delta(mark), string("Hello, world!\n")); // Zero columns means indeterminate width: emit unpadded. wrout.setMaxColumns(0); user_output->center("X"); - ARCHETYPE_TEST(strout2.getOutput().find("\nX\n") != string::npos - or strout2.getOutput().rfind("X\n") == strout2.getOutput().size() - 2); + ARCHETYPE_TEST_EQUAL(delta(mark), string("X\n")); + out() << "TestWrappedOutput::testCenter_ finished." << endl; }