From 995219d965b4201d8c61b2141271ce57328aad21 Mon Sep 17 00:00:00 2001 From: Josh Yelon Date: Tue, 12 Oct 2021 12:46:11 -0400 Subject: [PATCH] Move readline functionality into device-independent code --- luprex/core/cpp/drivenengine.cpp | 79 ++++++++++++++++++++++++++++++-- luprex/core/cpp/drivenengine.hpp | 26 +++++++++++ luprex/core/cpp/driver-mingw.cpp | 38 ++++----------- luprex/core/cpp/main.cpp | 1 + luprex/core/cpp/textgame.cpp | 19 ++++---- luprex/core/cpp/textgame.hpp | 1 + 6 files changed, 122 insertions(+), 42 deletions(-) diff --git a/luprex/core/cpp/drivenengine.cpp b/luprex/core/cpp/drivenengine.cpp index 0ed021e5..1d5a672d 100644 --- a/luprex/core/cpp/drivenengine.cpp +++ b/luprex/core/cpp/drivenengine.cpp @@ -9,6 +9,10 @@ Channel::Channel(DrivenEngine *de, int chid, int port, const std::string &target port_ = port; closed_ = false; target_ = target; + readline_enabled_ = (chid == 0); + readline_len_ = 0; + readline_lastc_ = 0; + echo_len_ = 0; assert(driven_->channels_[chid_] == nullptr); driven_->channels_[chid_] = this; } @@ -20,6 +24,50 @@ Channel::~Channel() { driven_->channels_[chid_] = nullptr; } + +void Channel::feed_readline(int nbytes, const char *bytes) { + for (int i = 0; i < nbytes; i++) { + char c = bytes[i]; + if ((c == '\n') && (readline_lastc_ == '\r')) { + // 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; + } + } 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; + } + } 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; + } + } + readline_lastc_ = c; + } +} + +void Channel::set_readline(bool e) { + if (e != readline_enabled_) { + readline_enabled_ = e; + readline_len_ = 0; + readline_lastc_ = 0; + echo_len_ = 0; + } +} + int DrivenEngine::find_unused_chid() { // Note: channel ID zero is special, it is never reused. for (int i = 0; i < MAX_CHAN; i++) { @@ -101,17 +149,40 @@ bool DrivenEngine::drv_outgoing_empty(int chid) { void DrivenEngine::drv_peek_outgoing(int chid, int *nbytes, const char **bytes) { Channel *ch = get_chid(chid); - *nbytes = ch->sb_out_->fill(); - *bytes = ch->sb_out_->data(); + if (ch->echo_len_ > 0) { + *nbytes = ch->echo_len_; + *bytes = ch->echo_buf_; + } else { + *nbytes = ch->sb_out_->fill(); + *bytes = ch->sb_out_->data(); + } } void DrivenEngine::drv_sent_outgoing(int chid, int nbytes) { - get_chid(chid)->sb_out_->read_bytes(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); + } + } } void DrivenEngine::drv_recv_incoming(int chid, int nbytes, const char *bytes) { if (nbytes > 0) { - get_chid(chid)->sb_in_->write_bytes(bytes, nbytes); + Channel *ch = get_chid(chid); + if (ch->readline_enabled_) { + ch->feed_readline(nbytes, bytes); + } else { + ch->sb_in_->write_bytes(bytes, nbytes); + } } } diff --git a/luprex/core/cpp/drivenengine.hpp b/luprex/core/cpp/drivenengine.hpp index 7af8e3da..3592b012 100644 --- a/luprex/core/cpp/drivenengine.hpp +++ b/luprex/core/cpp/drivenengine.hpp @@ -123,6 +123,20 @@ public: // bool closed() const { return closed_; } + // True if the channel is in readline mode. + // + // Stdio always starts with this enabled, other channels always start + // with this disabled. + // + bool readline_enabled() const { return readline_enabled_; } + + // Put the channel into readline mode. + // + // Caution: the channel better be coming from a raw tty, otherwise, + // this is going to produce weird results. + // + void set_readline(bool enabled); + // You may delete any channel except for stdio. This closes // the channel. // @@ -134,7 +148,13 @@ 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); + private: + static const int READLINE_MAX=512; DrivenEngine *driven_; int chid_; std::unique_ptr sb_in_; @@ -142,6 +162,12 @@ private: int port_; bool closed_; std::string target_; + char readline_buf_[READLINE_MAX]; + int readline_len_; + char readline_lastc_; + char echo_buf_[READLINE_MAX]; + int echo_len_; + bool readline_enabled_; friend class DrivenEngine; }; using UniqueChannel = std::unique_ptr; diff --git a/luprex/core/cpp/driver-mingw.cpp b/luprex/core/cpp/driver-mingw.cpp index 03136a9a..6bd61394 100644 --- a/luprex/core/cpp/driver-mingw.cpp +++ b/luprex/core/cpp/driver-mingw.cpp @@ -20,8 +20,6 @@ public: SOCKET socket_[MAX_CHAN]; bool connected_[MAX_CHAN]; bool engine_wakeup_; - char console_line_[CONSOLE_MAX + 1]; - int console_len_; std::map listen_sockets_; std::unique_ptr chbuf; @@ -113,7 +111,6 @@ public: socket_[i] = INVALID_SOCKET; connected_[i] = false; } - console_len_ = 0; engine_wakeup_ = false; chbuf.reset(new char[65536]); } @@ -182,16 +179,16 @@ public: } } - // This is painful. Win32 allows nonblocking read of keyboard events. But - // it doesn't have any way to do nonblocking read of processed lines. So we - // have to read individual events and do the line processing ourselves. + // We're feeding raw console characters to the DrivenEngine layer. + // The DrivenEngine channel is expected to be in readline mode, + // which will handle echoing and line processing. void handle_console_input() { HANDLE hstdin = GetStdHandle(STD_INPUT_HANDLE); - HANDLE hstdout = GetStdHandle(STD_OUTPUT_HANDLE); assert(hstdin != INVALID_HANDLE_VALUE); - assert(hstdout != INVALID_HANDLE_VALUE); INPUT_RECORD inrecords[512]; - DWORD nread, nevents, nwrote; + DWORD nread, nevents; + char ascii[512]; + int nascii = 0; if (GetNumberOfConsoleInputEvents(hstdin, &nevents)) { if (nevents > 512) nevents = 512; ReadConsoleInputA(hstdin, inrecords, nevents, &nread); @@ -201,28 +198,11 @@ public: const KEY_EVENT_RECORD &key = inr.Event.KeyEvent; if (!key.bKeyDown) continue; char c = key.uChar.AsciiChar; - if ((c == '\r') || (c == '\n')) { - console_line_[console_len_++] = '\n'; - assert(WriteConsoleA(hstdout, " \r\n", 3, &nwrote, nullptr)); - assert(nwrote==3); - driven_->drv_recv_incoming(0, console_len_, console_line_); - console_len_ = 0; - engine_wakeup_ = true; - } else if (c == '\b') { - if (console_len_ > 0) { - assert(WriteConsoleA(hstdout, "\b \b", 3, &nwrote, nullptr)); - assert(nwrote==3); - console_len_ -= 1; - } - } else if (c >= 32) { - if (console_len_ < CONSOLE_MAX) { - console_line_[console_len_++] = c; - assert(WriteConsoleA(hstdout, &c, 1, &nwrote, nullptr)); - assert(nwrote==1); - } - } + ascii[nascii++] = c; } + driven_->drv_recv_incoming(0, nascii, ascii); } + engine_wakeup_ = true; } void handle_clock() { diff --git a/luprex/core/cpp/main.cpp b/luprex/core/cpp/main.cpp index beb6d1c5..efeec0af 100644 --- a/luprex/core/cpp/main.cpp +++ b/luprex/core/cpp/main.cpp @@ -27,6 +27,7 @@ public: while (true) { UniqueChannel ch = new_incoming_channel(); if (ch == nullptr) break; + ch->set_readline(true); channels_.emplace_back(std::move(ch)); } diff --git a/luprex/core/cpp/textgame.cpp b/luprex/core/cpp/textgame.cpp index f8a9491a..d41c0899 100644 --- a/luprex/core/cpp/textgame.cpp +++ b/luprex/core/cpp/textgame.cpp @@ -170,18 +170,19 @@ void TextGame::check_redirects() { } } +void TextGame::event_init() +{ + world_.reset(new World(util::WORLD_TYPE_STANDALONE)); + world_->update_source(get_lua_source()); + world_->run_unittests(); + actor_id_ = world_->create_login_actor(); + std::cerr << "Login actor ID: " << actor_id_ << std::endl; + get_stdio_channel()->out()->write_bytes(console_.get_prompt()); +} + void TextGame::event_update() { - if (world_ == nullptr) { - world_.reset(new World(util::WORLD_TYPE_STANDALONE)); - world_->update_source(get_lua_source()); - world_->run_unittests(); - actor_id_ = world_->create_login_actor(); - std::cerr << "Login actor ID: " << actor_id_ << std::endl; - get_stdio_channel()->out()->write_bytes(console_.get_prompt()); - } world_->update_source(get_lua_source()); - while (true) { std::string line = get_stdio_channel()->in()->readline(); if (line == "") break; diff --git a/luprex/core/cpp/textgame.hpp b/luprex/core/cpp/textgame.hpp index f89bde85..5e7b07da 100644 --- a/luprex/core/cpp/textgame.hpp +++ b/luprex/core/cpp/textgame.hpp @@ -29,6 +29,7 @@ private: void check_redirects(); public: + virtual void event_init(); virtual void event_update(); };