The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
lua_interpreter.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2014 - 2025
3 by Chris Beck <render787@gmail.com>
4 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY.
12
13 See the COPYING file for more details.
14*/
15
16#define GETTEXT_DOMAIN "wesnoth-lib"
17
19
21#include "gui/widgets/label.hpp"
25
26#include "desktop/clipboard.hpp"
27#include "game_config.hpp"
28#include "game_errors.hpp"
29#include "gettext.hpp"
30#include "play_controller.hpp"
31#include "resources.hpp" //for help fetching lua kernel pointers
32#include "scripting/plugins/manager.hpp" //needed for the WHICH_KERNEL version of display
33#include "scripting/game_lua_kernel.hpp" //needed for the WHICH_KERNEL version of display
37#include "log.hpp"
38#include "font/pango/escape.hpp"
39
40#include <sstream>
41#include <string>
42#include <vector>
43#include <functional>
44
45#ifdef HAVE_HISTORY
46#include "filesystem.hpp"
47#include <readline/history.h>
48#endif
49
50static lg::log_domain log_lua_int("lua/interpreter");
51#define DBG_LUA LOG_STREAM(debug, log_lua_int)
52#define LOG_LUA LOG_STREAM(info, log_lua_int)
53#define WRN_LUA LOG_STREAM(warn, log_lua_int)
54#define ERR_LUA LOG_STREAM(err, log_lua_int)
55
56namespace gui2::dialogs
57{
58
59REGISTER_DIALOG(lua_interpreter)
60
61// Model, View, Controller definitions
62
64private:
65 scroll_label* msg_label; //the view is extremely simple, it's pretty much just this one widget that gets updated
67
68public:
69 view() : msg_label(nullptr), window_(nullptr) {}
70
71 /** Bind the scroll label widget to my pointer, and configure */
73 window_ = &window;
74 msg_label = window_->find_widget<scroll_label>("msg", false, true);
75 msg_label->set_use_markup(true);
77 msg_label->set_label("");
78 }
79
80 /** Update the scroll label contents */
81 void update_contents(const std::string & str)
82 {
83 assert(msg_label);
84 msg_label->set_label(str);
86 }
87
88 void pg_up()
89 {
90 assert(msg_label);
92 }
93
94 void pg_down()
95 {
96 assert(msg_label);
98 }
99};
100
101/**
102 * The lua model is responsible to interact with the lua kernel base and keep track of what should be displayed in the console.
103 * It registers its stringstream with the lua kernel when it is created, and unregisters when it is destroyed.
104 *
105 * It is responsible to execute commands as strings, or add dialog messages for the user. It is also responsible to ask
106 * the lua kernel for help with tab completion.
107 */
109private:
111 std::stringstream log_;
112 std::stringstream raw_log_;
113
114public:
116 : L_(lk)
117 , log_()
118 , raw_log_()
119 {
120 DBG_LUA << "constructing a lua_interpreter::model";
121 //DBG_LUA << "incoming:\n" << lk.get_log().rdbuf() << "\n.";
122 log_ << font::escape_text(lk.get_log().str()) << std::flush;
123 raw_log_ << lk.get_log().str() << std::flush;
124 // Lua kernel sends log strings to this function
125 L_.set_external_log([this](const std::string & str) {
126 log_ << font::escape_text(str);
127 raw_log_ << str;
128 });
129 //DBG_LUA << "received:\n" << log_.str() << "\n.";
130
131 DBG_LUA << "finished constructing a lua_interpreter::model";
132 }
133
135 {
136 DBG_LUA << "destroying a lua_interpreter::model";
137 L_.set_external_log(nullptr); //deregister our log since it's about to be destroyed
138 }
139
140 /** Ask the lua kernel to execute a command. No throw of game::lua_error, instead the error message is formatted and printed to console.*/
141 bool execute(const std::string & cmd);
142
143 /** Add a message from the dialog, formatted in blue to distinguish from issued commands.
144 * This message gets put in the interpreter log, but does not get entered in the kernel log, so if the window is closed this message will
145 * not appear the next time it is opened.
146 **/
147 void add_dialog_message(const std::string & msg);
148
149 /** Get the log string */
150 std::string get_log() const { return log_.str(); }
151 /** Get the unescaped log */
152 std::string get_raw_log() const { return raw_log_.str(); }
153 /** Get a string describing the name of lua kernel */
154 std::string get_name() const { return L_.my_name(); }
155
156 /** Clear the console log */
157 void clear_log() {
158 L_.clear_log();
159 log_.str("");
160 log_.clear();
161 raw_log_.str("");
162 raw_log_.clear();
163 }
164
165 //* Tab completion: Get list of presently defined global variables */
166 std::vector<std::string> get_globals() { return L_.get_global_var_names(); }
167 //* Tab completion: Get list of attributes for variable corresponding to this path. */
168 std::vector<std::string> get_attribute_names(const std::string & s) { return L_.get_attribute_names(s); }
169};
170
171/**
172 * The input_model keeps track of what commands were executed before, and figures out what
173 * should be displayed when the user presses up / down arrows in the input.
174 * It is essentially part of the model, but it isn't connected to the lua kernel so I have implemented it
175 * separately. Putatively it could all be refactored so that there is a single model with private subclass "lua_model"
176 * and also a "command_history_model" but I have decided simply to not implement it that way.
177 */
179private:
180 std::string prefix_;
182#ifdef HAVE_HISTORY
183 std::string filename_;
184#endif
185
186public:
188 : prefix_()
189 , end_of_history_(true)
190#ifdef HAVE_HISTORY
191 , filename_(filesystem::get_lua_history_file())
192 {
193 using_history();
194 read_history (filename_.c_str());
195 }
196#else
197 {}
198#endif
199
200#ifdef HAVE_HISTORY
201 ~input_model()
202 {
203 try {
204 const std::size_t history_max = 500;
206 append_history (history_max,filename_.c_str());
207 } else {
208 write_history (filename_.c_str());
209 }
210
211 history_truncate_file (filename_.c_str(), history_max);
212 } catch (...) { PLAIN_LOG << "Swallowed an exception when trying to write lua command line history";}
213 }
214#endif
215 void add_to_history ([[maybe_unused]] const std::string& str) {
216 prefix_ = "";
217#ifdef HAVE_HISTORY
218 add_history(str.c_str());
219#endif
220 end_of_history_ = true;
221
222 }
223
224 void maybe_update_prefix (const std::string & text) {
225 LOG_LUA << "maybe update prefix";
226 LOG_LUA << "prefix_: '"<< prefix_ << "'\t text='"<< text << "'";
227
228 if (!end_of_history_) return;
229
230 prefix_ = text;
231 LOG_LUA << "updated prefix";
232 }
233
234 std::string search([[maybe_unused]] int direction ) {
235#ifdef HAVE_HISTORY
236 LOG_LUA << "searching in direction " << direction << " from position " << where_history();
237
238 HIST_ENTRY * e = nullptr;
239 if (end_of_history_) {
240 // if the direction is > 0, do nothing because searching down only takes place when we are in the history records.
241 if (direction < 0) {
242 history_set_pos(history_length);
243
244 if (prefix_.size() > 0) {
245 int result = history_search_prefix(prefix_.c_str(), direction);
246 if (result == 0) {
247 e = current_history();
248 }
249 } else {
250 e = previous_history();
251 }
252 }
253 } else {
254 e = (direction > 0) ? next_history() : previous_history();
255 if (prefix_.size() > 0 && e) {
256 int result = history_search_prefix(prefix_.c_str(), direction);
257 if (result == 0) {
258 e = current_history();
259 } else {
260 e = nullptr; // if the search misses, it leaves the state as it was, which might not have been on an entry matching prefix.
261 end_of_history_ = true; // we actually want to force it to be null and treat as off the end of history in this case.
262 }
263 }
264 }
265
266 if (e) {
267 LOG_LUA << "found something at " << where_history();
268 std::string ret = e->line;
269 end_of_history_ = false;
270 return ret;
271 }
272#endif
273
274 LOG_LUA << "didn't find anything";
275
276 // reset, set history to the end and prefix_ to empty, and return the current prefix_ for the user to edit
277 end_of_history_ = true;
278 std::string temp = prefix_;
279 prefix_ = "";
280 return temp;
281 }
282
283 std::string clear_history() {
284#ifdef HAVE_HISTORY
286 write_history (filename_.c_str());
287 return "Cleared history.";
288#else
289 return "History is disabled, you did not compile with GNU history support.";
290#endif
291 }
292
293 std::string list_history() {
294#ifdef HAVE_HISTORY
295 HIST_ENTRY **the_list;
296
297 the_list = history_list ();
298 if (the_list) {
299 if (!*the_list) {
300 return "History is empty.";
301 }
302
303 std::string result;
304 for (int i = 0; the_list[i]; i++) {
305 result += std::to_string(i+history_base);
306 result += ": ";
307 result += the_list[i]->line;
308 result += "\n";
309 }
310 return result;
311 } else {
312 return "Couldn't find history.";
313 }
314#else
315 return "History is disabled, you did not compile with GNU history support.";
316#endif
317 }
318};
319
320/**
321 * The controller is responsible to hold all the input widgets, and a pointer to the model and view.
322 * It is responsible to bind the input signals to appropriate handler methods, which it holds.
323 * It is also responsible to ask the view to update based on the output of the model, typically in
324 * response to some input.
325 */
327private:
330
332 std::string text_entry_;
333
334 const std::unique_ptr<lua_interpreter::lua_model> lua_model_;
335 const std::unique_ptr<lua_interpreter::input_model> input_model_;
336 const std::unique_ptr<lua_interpreter::view> view_;
337
338 void execute();
339 void tab();
340 void search(int direction);
341public:
343 : copy_button(nullptr)
344 , clear_button(nullptr)
345 , text_entry(nullptr)
346 , text_entry_()
349 , view_(new lua_interpreter::view())
350 {}
351
352 /** Bind my pointers to the widgets found in the window */
353 void bind(window& window);
354
357
358 void input_keypress_callback(bool& handled,
359 bool& halt,
360 const SDL_Keycode key,
361 window& window);
362
363 /** Update the view based on the model */
364 void update_view();
365
366 friend class lua_interpreter;
367};
368
369// Model impl
370
371/** Execute a command, and report any errors encountered. */
372bool lua_interpreter::lua_model::execute (const std::string & cmd)
373{
374 LOG_LUA << "lua_interpreter::model::execute...";
375
376 try {
377 L_.interactive_run(cmd.c_str());
378 return true;
379 } catch (const game::lua_error & e) {
380 add_dialog_message(std::string(e.what()));
381 return false;
382 }
383}
384
385/** Add a dialog message, which will appear in blue. */
387 log_ << markup::span_color("#8888FF", font::escape_text(msg)) << "\n";
388 raw_log_ << msg << '\n';
389}
390
391// View impl
392
393// Controller impl
394
395/** Update the view based on the model. */
397{
398 LOG_LUA << "lua_interpreter update_view...";
399 assert(lua_model_);
400 assert(view_);
401
402 view_->update_contents(lua_model_->get_log());
403
404 LOG_LUA << "lua_interpreter update_view finished";
405}
406
407/** Find all the widgets managed by the controller and connect them to handler methods. */
409{
410 LOG_LUA << "Entering lua_interpreter::controller::bind";
411 assert(view_);
412 view_->bind(window);
413
414 text_entry = window.find_widget<text_box>("text_entry", false, true);
415 window.keyboard_capture(text_entry);
418
419 connect_signal_pre_key_press(
420 *text_entry,
422 this,
423 std::placeholders::_3,
424 std::placeholders::_4,
425 std::placeholders::_5,
426 std::ref(window)));
427
428 copy_button = window.find_widget<button>("copy", false, true);
429 connect_signal_mouse_left_click(
430 *copy_button,
432
433 clear_button = window.find_widget<button>("clear", false, true);
434 connect_signal_mouse_left_click(
435 *clear_button,
437
438 LOG_LUA << "Exiting lua_interpreter::controller::bind";
439}
440
441/** Copy text to the clipboard */
443{
444 assert(lua_model_);
445 desktop::clipboard::copy_to_clipboard(lua_model_->get_raw_log());
446}
447
448/** Clear the text */
450{
451 assert(lua_model_);
452 lua_model_->clear_log();
453 assert(view_);
454 view_->update_contents("");
455}
456
457/** Handle return key (execute) or tab key (tab completion) */
459 bool& halt,
460 const SDL_Keycode key,
461 window& window)
462{
463 assert(lua_model_);
464 assert(text_entry);
465
466 LOG_LUA << "keypress_callback";
467 if(key == SDLK_RETURN || key == SDLK_KP_ENTER) { // handle executing whatever is in the command entry field
468 LOG_LUA << "executing...";
469 execute();
470 handled = true;
471 halt = true;
472
473 // Commands such as `wesnoth.interface.zoom` might cause the display to redraw and leave the window half-drawn.
474 // This preempts that.
476
477 LOG_LUA << "finished executing";
478 } else if(key == SDLK_TAB) { // handle tab completion
479 tab();
480 handled = true;
481 halt = true;
482 } else if(key == SDLK_UP) {
483 search(-1);
484 handled = true;
485 halt = true;
486 } else if(key == SDLK_DOWN) {
487 search(1);
488 handled = true;
489 halt = true;
490 } else if(key == SDLK_PAGEUP) {
491 view_->pg_up();
492 handled = true;
493 halt = true;
494 } else if(key == SDLK_PAGEDOWN) {
495 view_->pg_down();
496 handled = true;
497 halt = true;
498 }
499}
500
502{
503 std::string cmd = text_entry->get_value();
504 if (cmd.empty()) return; //don't bother with empty string
505
506 cmd.erase(cmd.find_last_not_of(" \n\r\t")+1); //right trim the string
507
508 LOG_LUA << "Executing '"<< cmd << "'";
509
510 if (cmd.size() >= 13 && (cmd.substr(0,13) == "history clear" || cmd.substr(0,13) == "clear history")) {
511 lua_model_->add_dialog_message(input_model_->clear_history());
512 text_entry->set_value("");
513 update_view();
514 return;
515 }
516
517 if (cmd.size() >= 7 && (cmd.substr(0,7) == "history")) {
518 lua_model_->add_dialog_message(input_model_->list_history());
519 text_entry->set_value("");
520 update_view();
521 return;
522 }
523
524 if (lua_model_->execute(cmd)) {
525 input_model_->add_to_history(cmd);
526 text_entry->set_value("");
527 }
528 update_view();
529}
530
532{
533 std::string text = text_entry->get_value();
534
535 std::string prefix;
536 std::size_t prefix_end_pos = text.find_last_of(" (");
537 if (prefix_end_pos != std::string::npos) {
538 prefix = text.substr(0, prefix_end_pos + 1);
539 text = text.substr(prefix_end_pos + 1);
540 }
541
542 static std::vector<std::string> static_matches {
543 "and",
544 "break",
545 "else",
546 "elseif",
547 "end",
548 "false",
549 "for",
550 "function",
551 "local",
552 "nil",
553 "not",
554 "repeat",
555 "return",
556 "then",
557 "true",
558 "until",
559 "while"
560 };
561
562 std::vector<std::string> matches;
563
564 if (text.find('.') == std::string::npos) {
565 matches = lua_model_->get_globals();
566 matches.insert(matches.end(), static_matches.begin(), static_matches.end());
567 } else {
568 matches = lua_model_->get_attribute_names(text);
569 }
570
571 //bool line_start = utils::word_completion(text, matches);
572 if (text.size() > 0) { // this if is to avoid weird behavior in word_completion, where it thinks nothing matches the empty string
573 utils::word_completion(text, matches);
574 }
575
576 if(matches.empty()) {
577 return;
578 }
579
580 //if(matches.size() == 1) {
581 //text.append(" "); //line_start ? ": " : " ");
582 //} else {
583 if (matches.size() > 1) {
584 //std::string completion_list = utils::join(matches, " ");
585
586 const std::size_t wrap_limit = 80;
587 std::string buffer;
588
589 for (std::size_t idx = 0; idx < matches.size(); ++idx) {
590 if (buffer.size() + 1 + matches.at(idx).size() > wrap_limit) {
591 lua_model_->add_dialog_message(buffer);
592 buffer = matches.at(idx);
593 } else {
594 if (buffer.size()) {
595 buffer += (" " + matches.at(idx));
596 } else {
597 buffer = matches.at(idx);
598 }
599 }
600 }
601
602 lua_model_->add_dialog_message(buffer);
603 update_view();
604 }
605 text_entry->set_value(prefix + text);
606}
607
609{
610 std::string current_text = text_entry->get_value();
611 input_model_->maybe_update_prefix(current_text);
612 text_entry->set_value(input_model_->search(direction));
613
614#ifndef HAVE_HISTORY
615 lua_model_->add_dialog_message("History is disabled, you did not compile with GNU history support.");
616 update_view();
617#endif
618
619}
620
621// Dialog implementation
622
623/** Display a new console, using given video and lua kernel */
625#ifndef ALWAYS_HAVE_LUA_CONSOLE
628 const std::string& message = _("The lua console can only be used in debug mode! (Run ‘:debug’ first)");
629 chat_man.add_chat_message(std::chrono::system_clock::now(), _("lua console"), 0, message, events::chat_handler::MESSAGE_PRIVATE, false);
630 return;
631 }
632#endif
633 if (!lk) {
634 ERR_LUA << "Tried to open console with a null lua kernel pointer.";
635 return;
636 }
637
638 lua_interpreter(*lk).show();
639}
640
641/** Helper function to assist those callers which don't want to include resources.hpp */
643 if (which == lua_interpreter::APP) {
644 display(plugins_manager::get()->get_kernel_base());
645 } else if (which == lua_interpreter::GAME) {
647 }
648}
649
650/** Bind the controller, initialize one of the static labels with info about this kernel, and update the view. */
652{
653 LOG_LUA << "Entering lua_interpreter::view::pre_show";
654 register_text("text_entry", false, controller_->text_entry_, true);
655 controller_->bind(*this);
656
657 label *kernel_type_label = find_widget<label>("kernel_type", false, true);
658 kernel_type_label->set_label(controller_->lua_model_->get_name());
659
660 controller_->update_view();
661 //window.invalidate_layout(); // workaround for assertion failure
662 LOG_LUA << "Exiting lua_interpreter::view::pre_show";
663}
664
668{
669 LOG_LUA << "entering lua_interpreter ctor...";
670 LOG_LUA << "finished lua_interpreter ctor...";
671}
672
673} // namespace dialogs
std::string filename_
#define ERR_LUA
#define LOG_LUA
void add_chat_message(const std::chrono::system_clock::time_point &time, const std::string &speaker, int side, const std::string &msg, events::chat_handler::MESSAGE_TYPE type, bool bell)
Sort-of-Singleton that many classes, both GUI and non-GUI, use to access the game data.
Definition display.hpp:88
display_chat_manager & get_chat_manager()
Simple push button.
Definition button.hpp:36
The controller is responsible to hold all the input widgets, and a pointer to the model and view.
void handle_copy_button_clicked()
Copy text to the clipboard.
void update_view()
Update the view based on the model.
const std::unique_ptr< lua_interpreter::view > view_
void input_keypress_callback(bool &handled, bool &halt, const SDL_Keycode key, window &window)
Handle return key (execute) or tab key (tab completion)
const std::unique_ptr< lua_interpreter::lua_model > lua_model_
void bind(window &window)
Bind my pointers to the widgets found in the window.
const std::unique_ptr< lua_interpreter::input_model > input_model_
The input_model keeps track of what commands were executed before, and figures out what should be dis...
void maybe_update_prefix(const std::string &text)
The lua model is responsible to interact with the lua kernel base and keep track of what should be di...
bool execute(const std::string &cmd)
Ask the lua kernel to execute a command.
std::string get_log() const
Get the log string.
std::string get_name() const
Get a string describing the name of lua kernel.
void add_dialog_message(const std::string &msg)
Add a message from the dialog, formatted in blue to distinguish from issued commands.
std::string get_raw_log() const
Get the unescaped log.
std::vector< std::string > get_attribute_names(const std::string &s)
void bind(window &window)
Bind the scroll label widget to my pointer, and configure.
void update_contents(const std::string &str)
Update the scroll label contents.
virtual const std::string & window_id() const override
The ID of the window to build.
virtual void pre_show() override
Bind the controller, initialize one of the static labels with info about this kernel,...
static void display(lua_kernel_base *lk)
Display a new console, using given video and lua kernel.
const std::unique_ptr< controller > controller_
lua_interpreter(lua_kernel_base &lk)
Main class to show messages to the user.
Definition message.hpp:36
Abstract base class for all modal dialogs.
field_text * register_text(const std::string &id, const bool mandatory, const std::function< std::string()> &callback_load_value=nullptr, const std::function< void(const std::string &)> &callback_save_value=nullptr, const bool capture_focus=false)
Creates a new text field.
bool show(const unsigned auto_close_time=0)
Shows the window.
virtual void set_label(const t_string &label) override
See styled_widget::set_label.
virtual void set_use_markup(bool use_markup) override
See styled_widget::set_use_markup.
@ END
Go to the end position.
Definition scrollbar.hpp:58
@ JUMP_BACKWARDS
Go the visible items towards the begin.
Definition scrollbar.hpp:57
void scroll_vertical_scrollbar(const scrollbar_base::scroll_mode scroll)
Scrolls the vertical scrollbar.
@ ALWAYS_VISIBLE
The scrollbar is always shown, whether needed or not.
void set_vertical_scrollbar_mode(const scrollbar_mode scrollbar_mode)
virtual void set_label(const t_string &text)
A widget that allows the user to input text in single line.
Definition text_box.hpp:125
T * find_widget(const std::string_view id, const bool must_be_active, const bool must_exist)
Gets a widget with the wanted id.
Definition widget.hpp:747
void queue_redraw()
Indicates that this widget should be redrawn.
Definition widget.cpp:464
base class of top level items, the only item which needs to store the final canvases to draw on.
Definition window.hpp:59
void set_enter_disabled(const bool enter_disabled)
Disable the enter key.
Definition window.hpp:313
void keyboard_capture(widget *widget)
Definition window.cpp:1197
void set_click_dismiss(const bool click_dismiss)
Definition window.hpp:399
void set_external_log(external_log_type lg)
const std::stringstream & get_log()
Access / manipulate logging of lua kernel activities.
std::vector< std::string > get_global_var_names()
Get tab completion strings.
void interactive_run(char const *prog)
Tests if a program resolves to an expression, and pretty prints it if it is, otherwise it runs it nor...
std::vector< std::string > get_attribute_names(const std::string &var_path)
Gets all attribute names of an extended variable name.
virtual std::string my_name()
User-visible name of the lua kernel that they are talking to.
game_display & get_display() override
Get a reference to a display member a derived class uses.
static plugins_manager * get()
Definition manager.cpp:58
Declarations for File-IO.
static std::string _(const char *str)
Definition gettext.hpp:100
This file contains the window object, this object is a top level container which has the event manage...
Standard logging facilities (interface).
#define PLAIN_LOG
Definition log.hpp:296
static lg::log_domain log_lua_int("lua/interpreter")
#define DBG_LUA
#define REGISTER_DIALOG(window_id)
Wrapper for REGISTER_DIALOG2.
void copy_to_clipboard(const std::string &text)
Copies text to the clipboard.
Definition clipboard.cpp:24
bool file_exists(const std::string &name)
Returns true if a file or directory with such name already exists.
std::string escape_text(std::string_view text)
Escapes the pango markup characters in a text.
Definition escape.hpp:33
const bool & debug
std::string span_color(const color_t &color, Args &&... data)
Applies Pango markup to the input specifying its display color.
Definition markup.hpp:110
game_lua_kernel * lua_kernel
Definition resources.cpp:25
play_controller * controller
Definition resources.cpp:21
bool word_completion(std::string &text, std::vector< std::string > &wordlist)
Try to complete the last word of 'text' with the 'wordlist'.
Error used to report an error in a lua script or in the lua interpreter.
static map_location::direction s
#define i
#define e