add command history and history expansion to lua console
Adds an optional dependency on the readline library.
This commit is contained in:
parent
21e614c200
commit
ba46cc80b1
5 changed files with 324 additions and 91 deletions
|
@ -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)
|
||||
|
|
1
INSTALL
1
INSTALL
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -27,7 +27,8 @@ namespace gui2
|
|||
class tlua_interpreter : public tdialog
|
||||
{
|
||||
public:
|
||||
class model;
|
||||
class lua_model;
|
||||
class input_model;
|
||||
class view;
|
||||
class controller;
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue