From deaf4e4873cd116d13ade6b7f73b14c5189daa3e Mon Sep 17 00:00:00 2001 From: Josh Yelon Date: Thu, 4 Nov 2021 13:40:42 -0400 Subject: [PATCH] Implement more sophisticated readline-mode --- luprex/core/cpp/drivenengine.cpp | 132 ++++++++++++++++++++----------- luprex/core/cpp/drivenengine.hpp | 23 ++++-- luprex/core/cpp/driver-mingw.cpp | 5 +- luprex/core/cpp/drivertests.cpp | 21 +++-- luprex/core/cpp/textgame.cpp | 4 +- luprex/core/cpp/util.cpp | 23 ++++++ luprex/core/cpp/util.hpp | 6 ++ 7 files changed, 150 insertions(+), 64 deletions(-) diff --git a/luprex/core/cpp/drivenengine.cpp b/luprex/core/cpp/drivenengine.cpp index 9498cda1..c6d7a6c8 100644 --- a/luprex/core/cpp/drivenengine.cpp +++ b/luprex/core/cpp/drivenengine.cpp @@ -10,9 +10,8 @@ Channel::Channel(DrivenEngine *de, int chid, int port, const std::string &target closed_ = false; target_ = target; readline_enabled_ = (chid == 0); - readline_len_ = 0; readline_lastc_ = 0; - echo_len_ = 0; + desired_prompt_ = "] "; assert(driven_->channels_[chid_] == nullptr); driven_->channels_[chid_] = this; } @@ -24,6 +23,47 @@ Channel::~Channel() { driven_->channels_[chid_] = nullptr; } +void Channel::show_command() { + // If the prompt has changed, erase everything and start over. + if (desired_prompt_ != current_prompt_) { + int ccsize = current_prompt_.size() + current_command_.size(); + readline_echo_ += util::repeat_string("\b \b", ccsize); + readline_echo_ += desired_prompt_; + current_command_ = ""; + current_prompt_ = desired_prompt_; + } + + // Find out how much of the command matches. + int match = util::common_prefix_length(current_command_, desired_command_); + + // Echo backspaces to remove the non-matching part. + int remove = current_command_.size() - match; + if (remove > 0) { + readline_echo_ += util::repeat_string("\b \b", remove); + current_command_ = current_command_.substr(0, match); + } + + // Echo the new part. + std::string newpart = desired_command_.substr(current_command_.size()); + if (newpart != "") { + readline_echo_ += newpart; + current_command_ = desired_command_; + } +} + +void Channel::hide_command() { + int ccsize = current_command_.size() + current_prompt_.size(); + if (ccsize > 0) { + readline_echo_ += util::repeat_string("\b \b", ccsize); + current_command_ = ""; + current_prompt_ = ""; + } +} + +void Channel::set_prompt(const std::string &p) { + desired_prompt_ = p; + if (readline_enabled_) show_command(); +} void Channel::feed_readline(int nbytes, const char *bytes) { for (int i = 0; i < nbytes; i++) { @@ -32,39 +72,61 @@ void Channel::feed_readline(int nbytes, const char *bytes) { // Ignore newline immediately after carriage return. // Otherwise, crlf produces two newlines. } else if ((c == '\r') || (c == '\n')) { - if ((echo_space() >= 3) && (readline_space() >= 1)) { - echo_buf_[echo_len_++] = ' '; - echo_buf_[echo_len_++] = '\r'; - echo_buf_[echo_len_++] = '\n'; - readline_buf_[readline_len_++] = '\n'; - sb_in_->write_bytes(readline_buf_, readline_len_); - readline_len_ = 0; - } + show_command(); + readline_echo_ = readline_echo_ + " \n"; + sb_in_->write_bytes(desired_command_); + sb_in_->write_uint8('\n'); + desired_command_ = ""; + current_prompt_ = ""; + current_command_ = ""; } else if (c == '\b') { - if ((readline_len_ >= 1) && (echo_space() >= 3)) { - echo_buf_[echo_len_++] = '\b'; - echo_buf_[echo_len_++] = ' '; - echo_buf_[echo_len_++] = '\b'; - readline_len_ -= 1; + int len = desired_command_.size(); + if (len > 0) { + desired_command_ = desired_command_.substr(0, len-1); } } else if (c >= 32) { - // Don't use up the last character in the readline buffer: save - // it for the newline. - if ((readline_space() >= 2) && (echo_space() >= 1)) { - echo_buf_[echo_len_++] = c; - readline_buf_[readline_len_++] = c; + int len = desired_command_.size(); + if (len < READLINE_MAX) { + desired_command_ = desired_command_ + c; } } readline_lastc_ = c; } } +void Channel::peek_outgoing(int *nbytes, const char **bytes) { + if (sb_out_->fill() > 0) { + hide_command(); + } else if (readline_enabled_) { + show_command(); + } + if (readline_echo_.size() > 0) { + *nbytes = readline_echo_.size(); + *bytes = readline_echo_.c_str(); + } else { + *nbytes = sb_out_->fill(); + *bytes = sb_out_->data(); + } +} + +void Channel::sent_outgoing(int nbytes) { + if (nbytes > 0) { + if (readline_echo_.size() > 0) { + if (nbytes < int(readline_echo_.size())) { + readline_echo_ = readline_echo_.substr(nbytes); + return; + } + nbytes -= readline_echo_.size(); + readline_echo_ = ""; + } + sb_out_->read_bytes(nbytes); + } +} + void Channel::set_readline(bool e) { if (e != readline_enabled_) { readline_enabled_ = e; - readline_len_ = 0; - readline_lastc_ = 0; - echo_len_ = 0; + desired_command_ = ""; } } @@ -148,31 +210,11 @@ bool DrivenEngine::drv_outgoing_empty(int chid) { } void DrivenEngine::drv_peek_outgoing(int chid, int *nbytes, const char **bytes) { - Channel *ch = get_chid(chid); - if (ch->echo_len_ > 0) { - *nbytes = ch->echo_len_; - *bytes = ch->echo_buf_; - } else { - *nbytes = ch->sb_out_->fill(); - *bytes = ch->sb_out_->data(); - } + return get_chid(chid)->peek_outgoing(nbytes, bytes); } void DrivenEngine::drv_sent_outgoing(int chid, int nbytes) { - Channel *ch = get_chid(chid); - if (nbytes > 0) { - if (ch->echo_len_ > 0) { - if (nbytes >= ch->echo_len_) { - ch->sb_out_->read_bytes(nbytes - ch->echo_len_); - ch->echo_len_ = 0; - } else { - ch->echo_len_ -= nbytes; - memmove(ch->echo_buf_, ch->echo_buf_ + nbytes, ch->echo_len_); - } - } else { - ch->sb_out_->read_bytes(nbytes); - } - } + return get_chid(chid)->sent_outgoing(nbytes); } void DrivenEngine::drv_recv_incoming(int chid, int nbytes, const char *bytes) { diff --git a/luprex/core/cpp/drivenengine.hpp b/luprex/core/cpp/drivenengine.hpp index e110e7b1..dd4b213d 100644 --- a/luprex/core/cpp/drivenengine.hpp +++ b/luprex/core/cpp/drivenengine.hpp @@ -144,6 +144,10 @@ public: // void set_readline(bool enabled); + // Set the prompt for readline mode. + // + void set_prompt(const std::string &prompt); + // You may delete any channel except for stdio. This closes // the channel. // @@ -155,10 +159,11 @@ private: // Channel(DrivenEngine *de, int chid, int port, const std::string &target); - int readline_space() { return READLINE_MAX - readline_len_; } - int echo_space() { return READLINE_MAX - echo_len_; } - void feed_readline(int nbytes, const char *bytes); + void peek_outgoing(int *nbytes, const char **bytes); + void sent_outgoing(int nbytes); + void show_command(); + void hide_command(); private: static const int READLINE_MAX=512; @@ -170,12 +175,16 @@ private: bool closed_; std::string error_; std::string target_; - char readline_buf_[READLINE_MAX]; - int readline_len_; + + // Readline stuff. + std::string desired_command_; + std::string current_command_; + std::string desired_prompt_; + std::string current_prompt_; + std::string readline_echo_; char readline_lastc_; - char echo_buf_[READLINE_MAX]; - int echo_len_; bool readline_enabled_; + friend class DrivenEngine; }; diff --git a/luprex/core/cpp/driver-mingw.cpp b/luprex/core/cpp/driver-mingw.cpp index a40a3652..4b15a35d 100644 --- a/luprex/core/cpp/driver-mingw.cpp +++ b/luprex/core/cpp/driver-mingw.cpp @@ -176,8 +176,9 @@ public: void handle_console_output() { int nbytes; const char *bytes; - driven_->drv_peek_outgoing(0, &nbytes, &bytes); - if (nbytes > 0) { + while (true) { + driven_->drv_peek_outgoing(0, &nbytes, &bytes); + if (nbytes == 0) break; HANDLE hstdout = GetStdHandle(STD_OUTPUT_HANDLE); assert(hstdout != INVALID_HANDLE_VALUE); DWORD nwrote; diff --git a/luprex/core/cpp/drivertests.cpp b/luprex/core/cpp/drivertests.cpp index df51bc30..77edcd74 100644 --- a/luprex/core/cpp/drivertests.cpp +++ b/luprex/core/cpp/drivertests.cpp @@ -1,6 +1,5 @@ #include "drivertests.hpp" #include "drivenengine.hpp" -#include #include static void write_closed_message(Channel *ch, StreamBuffer *out) { @@ -107,17 +106,23 @@ public: // This test just prints the time. class DriverPrintClockTest : public DrivenEngine { public: - int count; + int count_; + double last_clock_; virtual void event_init(int argc, char *argv[]) { - count = 0; + count_ = 0; + last_clock_ = 0.0; } virtual void event_update() { - std::cerr << std::fixed << std::setprecision(2) << get_clock() << " "; - count++; - if (count == 10) { - std::cerr << std::endl; - count = 0; + double clock = get_clock(); + if (clock > last_clock_ + 0.5) { + stdostream() << std::fixed << std::setprecision(2) << clock << " "; + count_++; + last_clock_ = clock; + } + if (count_ == 10) { + stdostream() << std::endl; + count_ = 0; } } }; diff --git a/luprex/core/cpp/textgame.cpp b/luprex/core/cpp/textgame.cpp index 6600bb0f..de68ef71 100644 --- a/luprex/core/cpp/textgame.cpp +++ b/luprex/core/cpp/textgame.cpp @@ -210,7 +210,7 @@ void TextGame::event_init(int argc, char *argv[]) actor_id_ = world_->create_login_actor(); printbuffer_line_ = 0; stdostream() << "Login actor ID: " << actor_id_ << std::endl; - get_stdio_channel()->out()->write_bytes(console_.get_prompt()); + get_stdio_channel()->set_prompt(console_.get_prompt()); } void TextGame::event_update() @@ -231,7 +231,7 @@ void TextGame::event_update() stop_driver(); return; } - get_stdio_channel()->out()->write_bytes(console_.get_prompt()); + get_stdio_channel()->set_prompt(console_.get_prompt()); } } diff --git a/luprex/core/cpp/util.cpp b/luprex/core/cpp/util.cpp index 7a6b29bc..489a5ebb 100644 --- a/luprex/core/cpp/util.cpp +++ b/luprex/core/cpp/util.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #ifndef WIN32 #include @@ -141,6 +142,25 @@ StringVec split(const std::string &s, char sep) { return result; } +std::string repeat_string(const std::string &a, int n) { + int len = a.size(); + std::string result(len * n, ' '); + for (int i = 0; i < n; i++) { + for (int j = 0; j < len; j++) { + result[i*len + j] = a[j]; + } + } + return result; +} + +int common_prefix_length(const std::string &a, const std::string &b) { + int minlen = std::min(a.size(), b.size()); + for (int i = 0; i < minlen; i++) { + if (a[i] != b[i]) return i; + } + return minlen; +} + std::string tolower(std::string input) { for (int i = 0; i < int(input.size()); i++) { input[i] = std::tolower(input[i]); @@ -305,6 +325,9 @@ LuaDefine(unittests_util, "c") { LuaAssert(L, sv2[2]==""); LuaAssert(L, sv2[3]=="bar"); + // Test the repeat string routine. + LuaAssertStrEq(L, util::repeat_string("abc", 3), "abcabcabc"); + // test toupper and tolower LuaAssert(L, util::toupper("fooBar") == "FOOBAR"); LuaAssert(L, util::tolower("fooBar") == "foobar"); diff --git a/luprex/core/cpp/util.hpp b/luprex/core/cpp/util.hpp index 47017a24..3648ff93 100644 --- a/luprex/core/cpp/util.hpp +++ b/luprex/core/cpp/util.hpp @@ -56,6 +56,12 @@ std::string hash_to_hex(const HashValue &hash); // Split a string into multiple strings StringVec split(const std::string &s, char sep); +// Return N repetitions of string A +std::string repeat_string(const std::string &a, int n); + +// Return the length of the common prefix of A and B. +int common_prefix_length(const std::string &a, const std::string &b); + // String to lowercase/uppercase. Ascii only, no unicode. std::string tolower(std::string input); std::string toupper(std::string input);