Implement more sophisticated readline-mode

This commit is contained in:
2021-11-04 13:40:42 -04:00
parent cf5a2d7462
commit deaf4e4873
7 changed files with 150 additions and 64 deletions

View File

@@ -10,9 +10,8 @@ Channel::Channel(DrivenEngine *de, int chid, int port, const std::string &target
closed_ = false; closed_ = false;
target_ = target; target_ = target;
readline_enabled_ = (chid == 0); readline_enabled_ = (chid == 0);
readline_len_ = 0;
readline_lastc_ = 0; readline_lastc_ = 0;
echo_len_ = 0; desired_prompt_ = "] ";
assert(driven_->channels_[chid_] == nullptr); assert(driven_->channels_[chid_] == nullptr);
driven_->channels_[chid_] = this; driven_->channels_[chid_] = this;
} }
@@ -24,6 +23,47 @@ Channel::~Channel() {
driven_->channels_[chid_] = nullptr; 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) { void Channel::feed_readline(int nbytes, const char *bytes) {
for (int i = 0; i < nbytes; i++) { 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. // Ignore newline immediately after carriage return.
// Otherwise, crlf produces two newlines. // Otherwise, crlf produces two newlines.
} else if ((c == '\r') || (c == '\n')) { } else if ((c == '\r') || (c == '\n')) {
if ((echo_space() >= 3) && (readline_space() >= 1)) { show_command();
echo_buf_[echo_len_++] = ' '; readline_echo_ = readline_echo_ + " \n";
echo_buf_[echo_len_++] = '\r'; sb_in_->write_bytes(desired_command_);
echo_buf_[echo_len_++] = '\n'; sb_in_->write_uint8('\n');
readline_buf_[readline_len_++] = '\n'; desired_command_ = "";
sb_in_->write_bytes(readline_buf_, readline_len_); current_prompt_ = "";
readline_len_ = 0; current_command_ = "";
}
} else if (c == '\b') { } else if (c == '\b') {
if ((readline_len_ >= 1) && (echo_space() >= 3)) { int len = desired_command_.size();
echo_buf_[echo_len_++] = '\b'; if (len > 0) {
echo_buf_[echo_len_++] = ' '; desired_command_ = desired_command_.substr(0, len-1);
echo_buf_[echo_len_++] = '\b';
readline_len_ -= 1;
} }
} else if (c >= 32) { } else if (c >= 32) {
// Don't use up the last character in the readline buffer: save int len = desired_command_.size();
// it for the newline. if (len < READLINE_MAX) {
if ((readline_space() >= 2) && (echo_space() >= 1)) { desired_command_ = desired_command_ + c;
echo_buf_[echo_len_++] = c;
readline_buf_[readline_len_++] = c;
} }
} }
readline_lastc_ = 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) { void Channel::set_readline(bool e) {
if (e != readline_enabled_) { if (e != readline_enabled_) {
readline_enabled_ = e; readline_enabled_ = e;
readline_len_ = 0; desired_command_ = "";
readline_lastc_ = 0;
echo_len_ = 0;
} }
} }
@@ -148,31 +210,11 @@ bool DrivenEngine::drv_outgoing_empty(int chid) {
} }
void DrivenEngine::drv_peek_outgoing(int chid, int *nbytes, const char **bytes) { void DrivenEngine::drv_peek_outgoing(int chid, int *nbytes, const char **bytes) {
Channel *ch = get_chid(chid); return get_chid(chid)->peek_outgoing(nbytes, bytes);
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) { void DrivenEngine::drv_sent_outgoing(int chid, int nbytes) {
Channel *ch = get_chid(chid); return get_chid(chid)->sent_outgoing(nbytes);
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) { void DrivenEngine::drv_recv_incoming(int chid, int nbytes, const char *bytes) {

View File

@@ -144,6 +144,10 @@ public:
// //
void set_readline(bool enabled); 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 // You may delete any channel except for stdio. This closes
// the channel. // the channel.
// //
@@ -155,10 +159,11 @@ private:
// //
Channel(DrivenEngine *de, int chid, int port, const std::string &target); 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 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: private:
static const int READLINE_MAX=512; static const int READLINE_MAX=512;
@@ -170,12 +175,16 @@ private:
bool closed_; bool closed_;
std::string error_; std::string error_;
std::string target_; 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 readline_lastc_;
char echo_buf_[READLINE_MAX];
int echo_len_;
bool readline_enabled_; bool readline_enabled_;
friend class DrivenEngine; friend class DrivenEngine;
}; };

View File

@@ -176,8 +176,9 @@ public:
void handle_console_output() { void handle_console_output() {
int nbytes; const char *bytes; int nbytes; const char *bytes;
driven_->drv_peek_outgoing(0, &nbytes, &bytes); while (true) {
if (nbytes > 0) { driven_->drv_peek_outgoing(0, &nbytes, &bytes);
if (nbytes == 0) break;
HANDLE hstdout = GetStdHandle(STD_OUTPUT_HANDLE); HANDLE hstdout = GetStdHandle(STD_OUTPUT_HANDLE);
assert(hstdout != INVALID_HANDLE_VALUE); assert(hstdout != INVALID_HANDLE_VALUE);
DWORD nwrote; DWORD nwrote;

View File

@@ -1,6 +1,5 @@
#include "drivertests.hpp" #include "drivertests.hpp"
#include "drivenengine.hpp" #include "drivenengine.hpp"
#include <iostream>
#include <iomanip> #include <iomanip>
static void write_closed_message(Channel *ch, StreamBuffer *out) { static void write_closed_message(Channel *ch, StreamBuffer *out) {
@@ -107,17 +106,23 @@ public:
// This test just prints the time. // This test just prints the time.
class DriverPrintClockTest : public DrivenEngine { class DriverPrintClockTest : public DrivenEngine {
public: public:
int count; int count_;
double last_clock_;
virtual void event_init(int argc, char *argv[]) { virtual void event_init(int argc, char *argv[]) {
count = 0; count_ = 0;
last_clock_ = 0.0;
} }
virtual void event_update() { virtual void event_update() {
std::cerr << std::fixed << std::setprecision(2) << get_clock() << " "; double clock = get_clock();
count++; if (clock > last_clock_ + 0.5) {
if (count == 10) { stdostream() << std::fixed << std::setprecision(2) << clock << " ";
std::cerr << std::endl; count_++;
count = 0; last_clock_ = clock;
}
if (count_ == 10) {
stdostream() << std::endl;
count_ = 0;
} }
} }
}; };

View File

@@ -210,7 +210,7 @@ void TextGame::event_init(int argc, char *argv[])
actor_id_ = world_->create_login_actor(); actor_id_ = world_->create_login_actor();
printbuffer_line_ = 0; printbuffer_line_ = 0;
stdostream() << "Login actor ID: " << actor_id_ << std::endl; 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() void TextGame::event_update()
@@ -231,7 +231,7 @@ void TextGame::event_update()
stop_driver(); stop_driver();
return; return;
} }
get_stdio_channel()->out()->write_bytes(console_.get_prompt()); get_stdio_channel()->set_prompt(console_.get_prompt());
} }
} }

View File

@@ -8,6 +8,7 @@
#include <cmath> #include <cmath>
#include <iomanip> #include <iomanip>
#include <cassert> #include <cassert>
#include <algorithm>
#ifndef WIN32 #ifndef WIN32
#include <unistd.h> #include <unistd.h>
@@ -141,6 +142,25 @@ StringVec split(const std::string &s, char sep) {
return result; 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) { std::string tolower(std::string input) {
for (int i = 0; i < int(input.size()); i++) { for (int i = 0; i < int(input.size()); i++) {
input[i] = std::tolower(input[i]); input[i] = std::tolower(input[i]);
@@ -305,6 +325,9 @@ LuaDefine(unittests_util, "c") {
LuaAssert(L, sv2[2]==""); LuaAssert(L, sv2[2]=="");
LuaAssert(L, sv2[3]=="bar"); LuaAssert(L, sv2[3]=="bar");
// Test the repeat string routine.
LuaAssertStrEq(L, util::repeat_string("abc", 3), "abcabcabc");
// test toupper and tolower // test toupper and tolower
LuaAssert(L, util::toupper("fooBar") == "FOOBAR"); LuaAssert(L, util::toupper("fooBar") == "FOOBAR");
LuaAssert(L, util::tolower("fooBar") == "foobar"); LuaAssert(L, util::tolower("fooBar") == "foobar");

View File

@@ -56,6 +56,12 @@ std::string hash_to_hex(const HashValue &hash);
// Split a string into multiple strings // Split a string into multiple strings
StringVec split(const std::string &s, char sep); 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. // String to lowercase/uppercase. Ascii only, no unicode.
std::string tolower(std::string input); std::string tolower(std::string input);
std::string toupper(std::string input); std::string toupper(std::string input);