add command history and history expansion to lua console

Adds an optional dependency on the readline library.
This commit is contained in:
Chris Beck 2014-11-21 21:08:47 -05:00
parent 21e614c200
commit ba46cc80b1
5 changed files with 324 additions and 91 deletions

View file

@ -88,6 +88,7 @@ option(ENABLE_PANDORA "Add support for the OpenPandora by deactivating libvorbis
option(ENABLE_SDL_GPU "Enable building with SDL_gpu (experimental" OFF)
option(ENABLE_BOOST_FILESYSTEM "Enable building with the boost filesystem and boost locale code" ON)
option(ENABLE_LIBPNG "Enable support for writing png files (screenshots, images)" ON)
option(ENABLE_READLINE "Enable using readline for history in lua console" OFF)
if(NOT DEFINED ENABLE_DISPLAY_REVISION)
# Test whether the code is used in a repository if not autorevision will
@ -605,6 +606,13 @@ if(ENABLE_GAME)
else(ENABLE_LIBPNG AND PNG_FOUND)
message("Could not find lib PNG. Disabling support for writing PNG images.")
endif(ENABLE_LIBPNG AND PNG_FOUND)
find_package( READLINE )
if(ENABLE_READLINE AND READLINE_FOUND)
add_definitions(-DHAVE_READLINE)
else(ENABLE_READLINE AND READLINE_FOUND)
message("Could not find readline. Disabling support for command history in lua console.")
endif(ENABLE_READLINE AND READLINE_FOUND)
endif(ENABLE_GAME)
if(ENABLE_POT_UPDATE_TARGET)

View file

@ -38,6 +38,7 @@ These libraries are optional dependencies that enable additional features:
libdbus-1 (used for desktop notifications)
libpng (save images as pngs, otherwise only bmp is possible)
readline (for command history and history expansion in the lua console)
As a temporary compatibility measure, it is possible to build the game without requiring boost filesystem or boost locale, using libintl for translations instead. The cost is that wesnoth won't support UTF-8 in filepaths on all platforms.
For this, target filesystem.cpp and gettext.cpp instead of filesystem_boost.cpp and gettext_boost.cpp. For scons/cmake the "boost filesystem" option toggles these.

View file

@ -106,6 +106,7 @@ opts.AddVariables(
BoolVariable("fast", "Make scons faster at cost of less precise dependency tracking.", False),
BoolVariable("lockfile", "Create a lockfile to prevent multiple instances of scons from being run at the same time on this working copy.", False),
BoolVariable("OS_ENV", "Forward the entire OS environment to scons", False),
BoolVariable("readline", "Clear to disable readline support in lua console", False),
BoolVariable("sdl2", "Build with SDL2 support (experimental!)", False)
)
@ -414,6 +415,10 @@ if env["prereqs"]:
if env["png"]:
client_env.Append(CPPDEFINES = ["HAVE_LIBPNG"])
env["readline"] = env["readline"] and conf.CheckLib("readline")
if env["readline"]:
client_env.Append(CPPDEFINES = ["HAVE_READLINE"])
if env["forum_user_handler"]:
flags = env.ParseFlags("!mysql_config --libs --cflags")
try: # Some versions of mysql_config add -DNDEBUG but we don't want it

View file

@ -45,6 +45,11 @@
#include <boost/bind.hpp>
#include <boost/scoped_ptr.hpp>
#ifdef HAVE_READLINE
#include "filesystem.hpp"
#include <readline/history.h>
#endif
static lg::log_domain log_lua_int("lua/interpreter");
#define DBG_LUA LOG_STREAM(debug, log_lua_int)
#define LOG_LUA LOG_STREAM(info, log_lua_int)
@ -97,19 +102,19 @@ public:
};
/**
* The model is responsible to interact with the lua kernel base and keep track of what should be displayed in the console.
* The lua model is responsible to interact with the lua kernel base and keep track of what should be displayed in the console.
* It registers its stringstream with the lua kernel when it is created, and unregisters when it is destroyed.
*
* It is responsible to execute commands as strings, or add dialog messages for the user. It is also responsible to ask
* the lua kernel for help with tab completion.
*/
class tlua_interpreter::model {
class tlua_interpreter::lua_model {
private:
lua_kernel_base & L_;
std::stringstream log_;
public:
model (lua_kernel_base & lk)
lua_model (lua_kernel_base & lk)
: L_(lk)
, log_()
{
@ -122,7 +127,7 @@ public:
DBG_LUA << "finished constructing a tlua_interpreter::model\n";
}
~model()
~lua_model()
{
DBG_LUA << "destroying a tlua_interpreter::model\n";
L_.set_external_log(NULL); //deregister our log since it's about to be destroyed
@ -146,6 +151,116 @@ public:
std::vector<std::string> get_attribute_names(const std::string & s) { return L_.get_attribute_names(s); }
};
/**
* The input_model keeps track of what commands were executed before, and figures out what
* should be displayed when the user presses up / down arrows in the input.
* It is essentially part of the model, but it isn't connected to the lua kernel so I have implemented it
* separately. Putatively it could all be refactored so that there is a single model with private subclass "lua_model"
* and also a "command_history_model" but I have decided simply to not implement it that way.
*/
class tlua_interpreter::input_model {
private:
std::string prefix_;
bool end_of_history_;
public:
input_model()
: prefix_()
, end_of_history_(true)
{
#ifdef HAVE_READLINE
using_history();
read_history ((filesystem::get_user_config_dir() + "lua_command_history").c_str());
#endif
}
#ifdef HAVE_READLINE
~input_model()
{
try {
const size_t history_max = 500;
std::string filename = filesystem::get_user_config_dir() + "lua_command_history";
if (filesystem::file_exists(filename)) {
append_history (history_max,filename.c_str());
} else {
write_history (filename.c_str());
}
history_truncate_file ((filesystem::get_user_config_dir() + "lua_command_history").c_str(), history_max);
} catch (...) { std::cerr << "Swallowed an exception when trying to write lua command line history\n";}
}
#endif
void add_to_history (std::string str) {
prefix_ = "";
(void) str;
#ifdef HAVE_READLINE
add_history(str.c_str());
#endif
end_of_history_ = true;
}
void maybe_update_prefix (const std::string & text) {
LOG_LUA << "maybe update prefix\n";
LOG_LUA << "prefix_: '"<< prefix_ << "'\t text='"<< text << "'\n";
if (!end_of_history_) return;
prefix_ = text;
LOG_LUA << "updated prefix\n";
}
std::string search( int direction ) {
#ifdef HAVE_READLINE
LOG_LUA << "searching in direction " << direction << " from position " << where_history() << "\n";
HIST_ENTRY * e = NULL;
if (end_of_history_) {
// if the direction is > 0, do nothing because searching down only takes place when we are in the history records.
if (direction < 0) {
history_set_pos(history_length);
if (prefix_.size() > 0) {
int result = history_search_prefix(prefix_.c_str(), direction);
if (result == 0) {
e = current_history();
}
} else {
e = previous_history();
}
}
} else {
e = (direction > 0) ? next_history() : previous_history();
if (prefix_.size() > 0 && e) {
int result = history_search_prefix(prefix_.c_str(), direction);
if (result == 0) {
e = current_history();
} else {
e = NULL; // if the search misses, it leaves the state as it was, which might not have been on an entry matching prefix.
end_of_history_ = true; // we actually want to force it to be null and treat as off the end of history in this case.
}
}
}
if (e) {
LOG_LUA << "found something at " << where_history() << "\n";
std::string ret = e->line;
end_of_history_ = false;
return ret;
}
#endif
LOG_LUA << "didn't find anything\n";
// reset, set history to the end and prefix_ to empty, and return the current prefix_ for the user to edit
end_of_history_ = true;
(void) direction;
std::string temp = prefix_;
prefix_ = "";
return temp;
}
};
/**
* The controller is responsible to hold all the input widgets, and a pointer to the model and view.
* It is responsible to bind the input signals to appropriate handler methods, which it holds.
@ -159,14 +274,20 @@ private:
ttext_box* text_entry;
std::string text_entry_;
boost::scoped_ptr<tlua_interpreter::model> model_;
boost::scoped_ptr<tlua_interpreter::lua_model> lua_model_;
boost::scoped_ptr<tlua_interpreter::input_model> input_model_;
boost::scoped_ptr<tlua_interpreter::view> view_;
void execute();
void tab();
void search(int direction);
public:
controller(lua_kernel_base & lk)
: copy_button(NULL)
, text_entry(NULL)
, text_entry_()
, model_(new tlua_interpreter::model(lk))
, lua_model_(new tlua_interpreter::lua_model(lk))
, input_model_(new tlua_interpreter::input_model())
, view_(new tlua_interpreter::view())
{}
@ -188,7 +309,7 @@ public:
// Model impl
/** Execute a command, and report any errors encountered. */
bool tlua_interpreter::model::execute (const std::string & cmd)
bool tlua_interpreter::lua_model::execute (const std::string & cmd)
{
LOG_LUA << "tlua_interpreter::model::execute...\n";
try {
@ -201,7 +322,7 @@ bool tlua_interpreter::model::execute (const std::string & cmd)
}
/** Add a dialog message, which will appear in blue. */
void tlua_interpreter::model::add_dialog_message(const std::string & msg) {
void tlua_interpreter::lua_model::add_dialog_message(const std::string & msg) {
log_ << "<span color='#8888FF'>" << font::escape_text(msg) << "</span>\n";
}
@ -213,10 +334,10 @@ void tlua_interpreter::model::add_dialog_message(const std::string & msg) {
void tlua_interpreter::controller::update_view()
{
LOG_LUA << "tlua_interpreter update_view...\n";
assert(model_);
assert(lua_model_);
assert(view_);
view_->update_contents(model_->get_log());
view_->update_contents(lua_model_->get_log());
LOG_LUA << "tlua_interpreter update_view finished\n";
}
@ -263,8 +384,8 @@ void tlua_interpreter::controller::bind(twindow& window)
/** Copy text to the clipboard */
void tlua_interpreter::controller::handle_copy_button_clicked(twindow & /*window*/)
{
assert(model_);
desktop::clipboard::copy_to_clipboard(model_->get_log(), false);
assert(lua_model_);
desktop::clipboard::copy_to_clipboard(lua_model_->get_log(), false);
}
/** Handle return key (execute) or tab key (tab completion) */
@ -273,97 +394,194 @@ void tlua_interpreter::controller::input_keypress_callback(bool& handled,
const SDLKey key,
twindow& /*window*/)
{
assert(model_);
assert(lua_model_);
assert(text_entry);
LOG_LUA << "keypress_callback\n";
if(key == SDLK_RETURN || key == SDLK_KP_ENTER) { // handle executing whatever is in the command entry field
LOG_LUA << "executing...\n";
if (model_->execute(text_entry->get_value())) {
text_entry->save_to_history();
}
update_view();
text_entry->set_value("");
execute();
handled = true;
halt = true;
LOG_LUA << "finished executing\n";
} else if(key == SDLK_TAB) { // handle tab completion
std::string text = text_entry->get_value();
std::string prefix;
size_t idx = text.find_last_of(" (");
if (idx != std::string::npos) {
prefix = text.substr(0, idx+1);
text = text.substr(idx+1);
}
std::vector<std::string> matches;
if (text.find('.') == std::string::npos) {
matches = model_->get_globals();
matches.push_back("and");
matches.push_back("break");
matches.push_back("else");
matches.push_back("elseif");
matches.push_back("end");
matches.push_back("false");
matches.push_back("for");
matches.push_back("function");
matches.push_back("local");
matches.push_back("nil");
matches.push_back("not");
matches.push_back("repeat");
matches.push_back("return");
matches.push_back("then");
matches.push_back("true");
matches.push_back("until");
matches.push_back("while");
} else {
matches = model_->get_attribute_names(text);
}
//bool line_start = utils::word_completion(text, matches);
if (text.size() > 0) { // this if is to avoid wierd behavior in word_completion, where it thinks nothing matches the empty string
utils::word_completion(text, matches);
}
if(matches.empty()) {
handled = true; //there's no point in putting tabs in the command line
halt = true;
return;
}
//if(matches.size() == 1) {
//text.append(" "); //line_start ? ": " : " ");
//} else {
if (matches.size() > 1) {
//std::string completion_list = utils::join(matches, " ");
const size_t wrap_limit = 80;
std::string buffer;
for (size_t idx = 0; idx < matches.size(); ++idx) {
if (buffer.size() + 1 + matches.at(idx).size() > wrap_limit) {
model_->add_dialog_message(buffer);
buffer = matches.at(idx);
} else {
if (buffer.size()) {
buffer += (" " + matches.at(idx));
} else {
buffer = matches.at(idx);
}
}
}
model_->add_dialog_message(buffer);
update_view();
}
text_entry->set_value(prefix + text);
tab();
handled = true;
halt = true;
} else if(key == SDLK_UP) {
search(-1);
handled = true;
halt = true;
} else if(key == SDLK_DOWN) {
search(1);
handled = true;
halt = true;
}
}
void tlua_interpreter::controller::execute()
{
std::string cmd = text_entry->get_value();
if (cmd.size() == 0) return; //don't bother with empty string
cmd.erase(cmd.find_last_not_of(" \n\r\t")+1); //right trim the string
LOG_LUA << "Executing '"<< cmd << "'\n";
if (cmd.size() >= 13 && (cmd.substr(0,13) == "history clear" || cmd.substr(0,13) == "clear history")) {
#ifdef HAVE_READLINE
clear_history();
write_history ((filesystem::get_user_config_dir() + "lua_command_history").c_str());
text_entry->set_value("");
lua_model_->add_dialog_message("Cleared history.");
#else
lua_model_->add_dialog_message("History is disabled, you did not compile with readline support.");
#endif
update_view();
return;
}
if (cmd.size() >= 7 && (cmd.substr(0,7) == "history")) {
#ifdef HAVE_READLINE
std::string result;
HIST_ENTRY **the_list;
the_list = history_list ();
if (the_list) {
if (!*the_list) {
result += "History is empty.";
}
for (int i = 0; the_list[i]; i++) {
result += lexical_cast<std::string, int>(i+history_base);
result += ": ";
result += the_list[i]->line;
result += "\n";
}
} else {
result += "Couldn't find history.";
}
lua_model_->add_dialog_message(result);
#else
lua_model_->add_dialog_message("History is disabled, you did not compile with readline support.");
#endif
update_view();
return;
}
#ifdef HAVE_READLINE
// Do history expansions
char * cmd_cstr = new char [cmd.length()+1];
std::strcpy (cmd_cstr, cmd.c_str());
char * expansion;
int result = history_expand(cmd_cstr, &expansion);
free(cmd_cstr);
if (result < 0 || result == 2) {
lua_model_->add_dialog_message(expansion);
update_view();
free(expansion);
return ;
}
cmd = expansion;
free(expansion);
#endif
if (lua_model_->execute(cmd)) {
input_model_->add_to_history(cmd);
text_entry->set_value("");
}
update_view();
}
void tlua_interpreter::controller::tab()
{
std::string text = text_entry->get_value();
std::string prefix;
size_t idx = text.find_last_of(" (");
if (idx != std::string::npos) {
prefix = text.substr(0, idx+1);
text = text.substr(idx+1);
}
std::vector<std::string> matches;
if (text.find('.') == std::string::npos) {
matches = lua_model_->get_globals();
matches.push_back("and");
matches.push_back("break");
matches.push_back("else");
matches.push_back("elseif");
matches.push_back("end");
matches.push_back("false");
matches.push_back("for");
matches.push_back("function");
matches.push_back("local");
matches.push_back("nil");
matches.push_back("not");
matches.push_back("repeat");
matches.push_back("return");
matches.push_back("then");
matches.push_back("true");
matches.push_back("until");
matches.push_back("while");
} else {
matches = lua_model_->get_attribute_names(text);
}
//bool line_start = utils::word_completion(text, matches);
if (text.size() > 0) { // this if is to avoid wierd behavior in word_completion, where it thinks nothing matches the empty string
utils::word_completion(text, matches);
}
if(matches.empty()) {
return;
}
//if(matches.size() == 1) {
//text.append(" "); //line_start ? ": " : " ");
//} else {
if (matches.size() > 1) {
//std::string completion_list = utils::join(matches, " ");
const size_t wrap_limit = 80;
std::string buffer;
for (size_t idx = 0; idx < matches.size(); ++idx) {
if (buffer.size() + 1 + matches.at(idx).size() > wrap_limit) {
lua_model_->add_dialog_message(buffer);
buffer = matches.at(idx);
} else {
if (buffer.size()) {
buffer += (" " + matches.at(idx));
} else {
buffer = matches.at(idx);
}
}
}
lua_model_->add_dialog_message(buffer);
update_view();
}
text_entry->set_value(prefix + text);
}
void tlua_interpreter::controller::search(int direction)
{
std::string current_text = text_entry->get_value();
input_model_->maybe_update_prefix(current_text);
text_entry->set_value(input_model_->search(direction));
#ifndef HAVE_READLINE
lua_model_->add_dialog_message("History is disabled, you did not compile with readline support.");
update_view();
#endif
}
// Dialog implementation
/** Display a new console, using given video and lua kernel */
@ -399,7 +617,7 @@ void tlua_interpreter::pre_show(CVideo& /*video*/, twindow& window)
controller_->bind(window);
tlabel *kernel_type_label = &find_widget<tlabel>(&window, "kernel_type", false);
kernel_type_label->set_label(controller_->model_->get_name());
kernel_type_label->set_label(controller_->lua_model_->get_name());
controller_->update_view();
//window.invalidate_layout(); // workaround for assertion failure

View file

@ -27,7 +27,8 @@ namespace gui2
class tlua_interpreter : public tdialog
{
public:
class model;
class lua_model;
class input_model;
class view;
class controller;