2023-01-07 11:24:02 -05:00
|
|
|
#include "util.h"
|
2021-03-07 23:41:21 -05:00
|
|
|
|
|
|
|
#include "common/util/FileUtil.h"
|
2023-01-07 11:24:02 -05:00
|
|
|
#include "common/util/json_util.h"
|
|
|
|
#include "common/util/string_util.h"
|
2023-04-22 14:13:57 -04:00
|
|
|
#include "common/versions/versions.h"
|
2022-06-22 23:37:46 -04:00
|
|
|
|
2021-03-07 23:41:21 -05:00
|
|
|
#include "third-party/fmt/color.h"
|
|
|
|
#include "third-party/fmt/core.h"
|
2022-06-22 23:37:46 -04:00
|
|
|
#include "third-party/replxx/include/replxx.hxx"
|
2021-03-07 23:41:21 -05:00
|
|
|
// TODO - expand a list of hints (ie. a hint for defun to show at a glance how to write a function,
|
|
|
|
// or perhaps, show the docstring for the current function being used?)
|
|
|
|
|
2023-01-07 11:24:02 -05:00
|
|
|
namespace REPL {
|
|
|
|
void Wrapper::clear_screen() {
|
2021-03-07 23:41:21 -05:00
|
|
|
repl.clear_screen();
|
|
|
|
}
|
|
|
|
|
2023-01-07 11:24:02 -05:00
|
|
|
void Wrapper::print_welcome_message() {
|
2022-05-06 18:19:37 -04:00
|
|
|
// TODO - dont print on std-out
|
2021-03-07 23:41:21 -05:00
|
|
|
// Welcome message / brief intro for documentation
|
|
|
|
std::string ascii;
|
|
|
|
ascii += " _____ _____ _____ _____ __ \n";
|
|
|
|
ascii += "| |___ ___ ___| __| | _ | | \n";
|
|
|
|
ascii += "| | | . | -_| | | | | | | |__ \n";
|
|
|
|
ascii += "|_____| _|___|_|_|_____|_____|__|__|_____|\n";
|
|
|
|
ascii += " |_| \n";
|
|
|
|
fmt::print(fmt::emphasis::bold | fg(fmt::color::orange), ascii);
|
|
|
|
|
|
|
|
fmt::print("Welcome to OpenGOAL {}.{}!\n", versions::GOAL_VERSION_MAJOR,
|
|
|
|
versions::GOAL_VERSION_MINOR);
|
2023-01-07 11:24:02 -05:00
|
|
|
fmt::print("Run {} or {} for help with common commands and REPL usage.\n",
|
|
|
|
fmt::styled("(repl-help)", fmt::emphasis::bold | fg(fmt::color::cyan)),
|
|
|
|
fmt::styled("(repl-keybinds)", fmt::emphasis::bold | fg(fmt::color::cyan)));
|
2021-03-07 23:41:21 -05:00
|
|
|
fmt::print("Run ");
|
|
|
|
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(lt)");
|
2022-06-30 21:11:58 -04:00
|
|
|
fmt::print(" to connect to the local target.\n");
|
|
|
|
fmt::print("Run ");
|
|
|
|
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(mi)");
|
|
|
|
fmt::print(" to rebuild the entire game.\n\n");
|
2021-03-07 23:41:21 -05:00
|
|
|
}
|
|
|
|
|
2023-01-07 11:24:02 -05:00
|
|
|
void Wrapper::print_to_repl(const std::string& str) {
|
2022-05-06 18:19:37 -04:00
|
|
|
repl.print(str.data());
|
|
|
|
}
|
|
|
|
|
2023-01-07 11:24:02 -05:00
|
|
|
void Wrapper::set_history_max_size(size_t len) {
|
2021-03-07 23:41:21 -05:00
|
|
|
repl.set_max_history_size(len);
|
|
|
|
}
|
|
|
|
|
2023-01-07 11:24:02 -05:00
|
|
|
const char* Wrapper::readline(const std::string& prompt) {
|
2021-03-07 23:41:21 -05:00
|
|
|
return repl.input(prompt);
|
|
|
|
}
|
|
|
|
|
2023-01-07 11:24:02 -05:00
|
|
|
void Wrapper::add_to_history(const std::string& line) {
|
2021-03-07 23:41:21 -05:00
|
|
|
repl.history_add(line);
|
|
|
|
}
|
|
|
|
|
2023-01-07 11:24:02 -05:00
|
|
|
void Wrapper::save_history() {
|
2023-07-05 14:15:46 -04:00
|
|
|
fs::path path;
|
|
|
|
if (repl_config.per_game_history) {
|
|
|
|
path = file_util::get_user_config_dir() / game_version_names[repl_config.game_version] /
|
|
|
|
".opengoal.repl.history";
|
|
|
|
} else {
|
|
|
|
path = file_util::get_user_config_dir() / ".opengoal.repl.history";
|
|
|
|
}
|
2022-07-02 15:32:52 -04:00
|
|
|
file_util::create_dir_if_needed_for_file(path.string());
|
|
|
|
repl.history_save(path.string());
|
2021-03-07 23:41:21 -05:00
|
|
|
}
|
|
|
|
|
2023-01-07 11:24:02 -05:00
|
|
|
void Wrapper::load_history() {
|
2023-07-05 14:15:46 -04:00
|
|
|
fs::path path;
|
|
|
|
if (repl_config.per_game_history) {
|
|
|
|
path = file_util::get_user_config_dir() / game_version_names[repl_config.game_version] /
|
|
|
|
".opengoal.repl.history";
|
|
|
|
} else {
|
|
|
|
path = file_util::get_user_config_dir() / ".opengoal.repl.history";
|
|
|
|
}
|
2022-07-05 20:38:13 -04:00
|
|
|
if (fs::exists(path)) {
|
2022-06-30 18:05:26 -04:00
|
|
|
repl.history_load(path.string());
|
|
|
|
} else {
|
2022-07-02 15:32:52 -04:00
|
|
|
fmt::print("Couldn't locate REPL history file at '{}'\n", path.string());
|
2021-03-07 23:41:21 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-07 11:24:02 -05:00
|
|
|
std::pair<std::string, bool> Wrapper::get_current_repl_token(std::string const& context) {
|
2021-03-07 23:41:21 -05:00
|
|
|
// Find the current token
|
|
|
|
std::string token = "";
|
|
|
|
for (auto c = context.crbegin(); c != context.crend(); c++) {
|
|
|
|
if (std::isspace(*c)) {
|
|
|
|
break;
|
|
|
|
} else {
|
|
|
|
token = *c + token;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If there is a preceeding '(' remove it
|
|
|
|
if (!token.empty() && token.at(0) == '(') {
|
|
|
|
token.erase(0, 1);
|
|
|
|
return {token, true};
|
|
|
|
}
|
|
|
|
return {token, false};
|
|
|
|
}
|
|
|
|
|
2023-01-07 11:24:02 -05:00
|
|
|
void Wrapper::print_help_message() {
|
2021-03-07 23:41:21 -05:00
|
|
|
fmt::print(fmt::emphasis::bold, "\nREPL Controls:\n");
|
|
|
|
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(:clear)\n");
|
|
|
|
fmt::print(" - Clear the current screen\n");
|
|
|
|
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(e)\n");
|
2022-06-30 21:11:58 -04:00
|
|
|
fmt::print(" - Exit the compiler\n");
|
2021-03-07 23:41:21 -05:00
|
|
|
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(lt [ip-address] [port-number])\n");
|
|
|
|
fmt::print(
|
|
|
|
" - Connect the listener to a running target. The IP address defaults to `127.0.0.1` and the "
|
|
|
|
"port to `8112`\n");
|
|
|
|
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(r [ip-address] [port-number])\n");
|
|
|
|
fmt::print(
|
|
|
|
" - Attempt to reset the target and reconnect. After this, the target will have nothing "
|
|
|
|
"loaded.\n");
|
|
|
|
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(:status)\n");
|
|
|
|
fmt::print(" - Send a ping-like message to the target. Requires the target to be connected\n");
|
|
|
|
fmt::print(fmt::emphasis::bold | fg(fmt::color::cyan), "(shutdown-target)\n");
|
|
|
|
fmt::print(" - If the target is connected, make it exit\n");
|
|
|
|
|
|
|
|
fmt::print(fmt::emphasis::bold, "\nCompiling & Building:\n");
|
2022-06-30 21:11:58 -04:00
|
|
|
fmt::print(fmt::emphasis::bold | fg(fmt::color::lime_green), "(mi)\n");
|
|
|
|
fmt::print(" - Build entire game\n");
|
|
|
|
fmt::print(fmt::emphasis::bold | fg(fmt::color::lime_green), "(mng)\n");
|
|
|
|
fmt::print(" - Build game engine\n");
|
2021-03-07 23:41:21 -05:00
|
|
|
fmt::print(fmt::emphasis::bold | fg(fmt::color::lime_green), "(m \"filename\")\n");
|
|
|
|
fmt::print(" - Compile an OpenGOAL source file\n");
|
|
|
|
fmt::print(fmt::emphasis::bold | fg(fmt::color::lime_green), "(ml \"filename\")\n");
|
2022-06-30 21:11:58 -04:00
|
|
|
fmt::print(" - Compile and Load (or reload) an OpenGOAL source file\n");
|
2021-03-07 23:41:21 -05:00
|
|
|
fmt::print(fmt::emphasis::bold | fg(fmt::color::lime_green), "(build-kernel)\n");
|
2022-06-30 21:11:58 -04:00
|
|
|
fmt::print(" - Build the GOAL kernel\n");
|
|
|
|
fmt::print(fmt::emphasis::bold | fg(fmt::color::lime_green), "(make \"file-name\")\n");
|
2021-03-07 23:41:21 -05:00
|
|
|
fmt::print(
|
2022-06-30 21:11:58 -04:00
|
|
|
" - Build a file and any out-of-date dependencies. This file must be a target in the make "
|
|
|
|
"system.\n");
|
2021-03-07 23:41:21 -05:00
|
|
|
|
|
|
|
fmt::print(fmt::emphasis::bold, "\nOther:\n");
|
|
|
|
fmt::print(fmt::emphasis::bold | fg(fmt::color::magenta), "(gs)\n");
|
|
|
|
fmt::print(" - Enter a GOOS REPL\n");
|
|
|
|
}
|
|
|
|
|
2023-01-07 11:24:02 -05:00
|
|
|
void Wrapper::print_keybind_help() {
|
|
|
|
fmt::print(fmt::emphasis::bold, "\nREPL KeyBinds:\n");
|
|
|
|
for (const auto& bind : repl_config.keybinds) {
|
|
|
|
fmt::print("{}\n", fmt::styled(bind.string(), fmt::fg(fmt::color::cyan)));
|
|
|
|
fmt::print("{}\n", fmt::styled(bind.description, fmt::fg(fmt::color::gray)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
replxx::Replxx::key_press_handler_t Wrapper::commit_text_action(std::string text_to_commit) {
|
repl: Add a few new quality of life improvements (#2030)
- You can define a `startup.gc` in your user folder, each line will be
executed on startup (deprecates the usefulness of some cli flags)
- You can define a `repl-config.json` file to override REPL settings.
Long-term this is a better approach than a bunch of CLI flags as well
- Via this, you can override the amount of time the repl will attempt to
listen for the target
- At the same time, I think i may have found why on Windows it can
sometimes take forever to timeout when the game dies, will dig into this
later
- Added some keybinds for common operations, shown here
https://user-images.githubusercontent.com/13153231/202890278-1ff2bb06-dddf-4bde-9178-aa0883799167.mp4
> builds the game, connects to it, attaches a debugger and continues,
launches it, gets the backtrace, stops the target -- all with only
keybinds.
If you want these keybinds to work inside VSCode's integrated terminal,
you need to add the following to your settings file
```json
"terminal.integrated.commandsToSkipShell": [
"-workbench.action.quickOpen",
"-workbench.action.quickOpenView"
]
```
2022-11-20 14:28:41 -05:00
|
|
|
return [this, text_to_commit](char32_t code) {
|
|
|
|
repl.set_state(
|
|
|
|
replxx::Replxx::State(text_to_commit.c_str(), static_cast<int>(text_to_commit.size())));
|
2023-01-07 11:24:02 -05:00
|
|
|
return repl.invoke(replxx::Replxx::ACTION::COMMIT_LINE, code);
|
repl: Add a few new quality of life improvements (#2030)
- You can define a `startup.gc` in your user folder, each line will be
executed on startup (deprecates the usefulness of some cli flags)
- You can define a `repl-config.json` file to override REPL settings.
Long-term this is a better approach than a bunch of CLI flags as well
- Via this, you can override the amount of time the repl will attempt to
listen for the target
- At the same time, I think i may have found why on Windows it can
sometimes take forever to timeout when the game dies, will dig into this
later
- Added some keybinds for common operations, shown here
https://user-images.githubusercontent.com/13153231/202890278-1ff2bb06-dddf-4bde-9178-aa0883799167.mp4
> builds the game, connects to it, attaches a debugger and continues,
launches it, gets the backtrace, stops the target -- all with only
keybinds.
If you want these keybinds to work inside VSCode's integrated terminal,
you need to add the following to your settings file
```json
"terminal.integrated.commandsToSkipShell": [
"-workbench.action.quickOpen",
"-workbench.action.quickOpenView"
]
```
2022-11-20 14:28:41 -05:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-01-07 11:24:02 -05:00
|
|
|
void Wrapper::init_settings() {
|
repl: Add a few new quality of life improvements (#2030)
- You can define a `startup.gc` in your user folder, each line will be
executed on startup (deprecates the usefulness of some cli flags)
- You can define a `repl-config.json` file to override REPL settings.
Long-term this is a better approach than a bunch of CLI flags as well
- Via this, you can override the amount of time the repl will attempt to
listen for the target
- At the same time, I think i may have found why on Windows it can
sometimes take forever to timeout when the game dies, will dig into this
later
- Added some keybinds for common operations, shown here
https://user-images.githubusercontent.com/13153231/202890278-1ff2bb06-dddf-4bde-9178-aa0883799167.mp4
> builds the game, connects to it, attaches a debugger and continues,
launches it, gets the backtrace, stops the target -- all with only
keybinds.
If you want these keybinds to work inside VSCode's integrated terminal,
you need to add the following to your settings file
```json
"terminal.integrated.commandsToSkipShell": [
"-workbench.action.quickOpen",
"-workbench.action.quickOpenView"
]
```
2022-11-20 14:28:41 -05:00
|
|
|
// NOTE - a nice popular project that uses replxx
|
|
|
|
// - https://github.com/ClickHouse/ClickHouse/blob/master/base/base/ReplxxLineReader.cpp#L366
|
2021-03-07 23:41:21 -05:00
|
|
|
repl.set_word_break_characters(" \t");
|
2023-06-29 16:32:48 -04:00
|
|
|
repl.set_complete_on_empty(false);
|
|
|
|
repl.set_indent_multiline(false);
|
|
|
|
repl.enable_bracketed_paste();
|
repl: Add a few new quality of life improvements (#2030)
- You can define a `startup.gc` in your user folder, each line will be
executed on startup (deprecates the usefulness of some cli flags)
- You can define a `repl-config.json` file to override REPL settings.
Long-term this is a better approach than a bunch of CLI flags as well
- Via this, you can override the amount of time the repl will attempt to
listen for the target
- At the same time, I think i may have found why on Windows it can
sometimes take forever to timeout when the game dies, will dig into this
later
- Added some keybinds for common operations, shown here
https://user-images.githubusercontent.com/13153231/202890278-1ff2bb06-dddf-4bde-9178-aa0883799167.mp4
> builds the game, connects to it, attaches a debugger and continues,
launches it, gets the backtrace, stops the target -- all with only
keybinds.
If you want these keybinds to work inside VSCode's integrated terminal,
you need to add the following to your settings file
```json
"terminal.integrated.commandsToSkipShell": [
"-workbench.action.quickOpen",
"-workbench.action.quickOpenView"
]
```
2022-11-20 14:28:41 -05:00
|
|
|
// Setup default keybinds
|
2023-01-07 11:24:02 -05:00
|
|
|
for (const auto& bind : repl_config.keybinds) {
|
|
|
|
char32_t code;
|
|
|
|
switch (bind.modifier) {
|
|
|
|
case KeyBind::Modifier::CTRL:
|
|
|
|
code = replxx::Replxx::KEY::control(bind.key.at(0));
|
|
|
|
break;
|
|
|
|
case KeyBind::Modifier::SHIFT:
|
|
|
|
code = replxx::Replxx::KEY::shift(bind.key.at(0));
|
|
|
|
break;
|
|
|
|
case KeyBind::Modifier::META:
|
|
|
|
code = replxx::Replxx::KEY::meta(bind.key.at(0));
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
repl.bind_key(code, commit_text_action(bind.command));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Wrapper::reload_startup_file() {
|
2023-01-30 20:45:03 -05:00
|
|
|
startup_file = load_user_startup_file(username, repl_config.game_version);
|
2023-01-07 11:24:02 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
std::string find_repl_username() {
|
|
|
|
// Two options - either:
|
|
|
|
// 1. look for the `user.txt` file, which should only contain the username
|
|
|
|
// 2. if this is absent AND there is a single folder inside the "user" folder, use that as the
|
|
|
|
// username
|
|
|
|
auto user_dir = file_util::get_jak_project_dir() / "goal_src" / "user";
|
|
|
|
auto dirs = file_util::find_directories_in_dir(user_dir);
|
|
|
|
if (dirs.size() == 1) {
|
|
|
|
return dirs.at(0).filename().string();
|
|
|
|
}
|
|
|
|
|
|
|
|
std::regex allowed_chars("(^[0-9a-zA-Z\\-\\.\\!\\?<>]*$)");
|
|
|
|
if (file_util::file_exists((user_dir / "user.txt").string())) {
|
|
|
|
auto text = file_util::read_text_file(user_dir / "user.txt");
|
|
|
|
text = str_util::trim(text);
|
|
|
|
if (!text.empty() && std::regex_match(text, allowed_chars)) {
|
|
|
|
return text;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-30 23:28:30 -04:00
|
|
|
return "unknown";
|
2023-01-07 11:24:02 -05:00
|
|
|
}
|
|
|
|
|
2023-01-30 20:45:03 -05:00
|
|
|
fs::path get_startup_file_path(const std::string& username, const GameVersion game_version) {
|
|
|
|
// - first check to see if there is a game version specific startup file to prefer
|
|
|
|
auto game_specific_path = file_util::get_jak_project_dir() / "goal_src" / "user" / username /
|
|
|
|
fmt::format("startup-{}.gc", version_to_game_name(game_version));
|
|
|
|
if (file_util::file_exists(game_specific_path.string())) {
|
|
|
|
return game_specific_path;
|
|
|
|
}
|
|
|
|
return file_util::get_jak_project_dir() / "goal_src" / "user" / username / "startup.gc";
|
|
|
|
}
|
|
|
|
|
|
|
|
StartupFile load_user_startup_file(const std::string& username, const GameVersion game_version) {
|
2023-01-07 11:24:02 -05:00
|
|
|
// Check for a `startup.gc` file, each line will be executed on the REPL on startup
|
2023-01-30 20:45:03 -05:00
|
|
|
auto startup_file_path = get_startup_file_path(username, game_version);
|
2023-01-07 11:24:02 -05:00
|
|
|
StartupFile startup_file;
|
|
|
|
if (file_util::file_exists(startup_file_path.string())) {
|
|
|
|
auto data = file_util::read_text_file(startup_file_path);
|
|
|
|
auto startup_cmds = str_util::split(data);
|
|
|
|
bool found_run_on_listen_line = false;
|
|
|
|
for (const auto& cmd : startup_cmds) {
|
|
|
|
if (found_run_on_listen_line) {
|
|
|
|
startup_file.run_after_listen.push_back(cmd);
|
|
|
|
} else {
|
|
|
|
startup_file.run_before_listen.push_back(cmd);
|
|
|
|
}
|
|
|
|
if (str_util::contains(cmd, "og:run-below-on-listen")) {
|
|
|
|
found_run_on_listen_line = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return startup_file;
|
|
|
|
}
|
|
|
|
|
|
|
|
REPL::Config load_repl_config(const std::string& username, const GameVersion game_version) {
|
|
|
|
auto repl_config_path =
|
|
|
|
file_util::get_jak_project_dir() / "goal_src" / "user" / username / "repl-config.json";
|
|
|
|
if (file_util::file_exists(repl_config_path.string())) {
|
|
|
|
try {
|
|
|
|
REPL::Config config(game_version);
|
|
|
|
auto repl_config_data =
|
|
|
|
parse_commented_json(file_util::read_text_file(repl_config_path), "repl-config.json");
|
|
|
|
from_json(repl_config_data, config);
|
|
|
|
return config;
|
|
|
|
} catch (std::exception& e) {
|
|
|
|
REPL::Config config(game_version);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return REPL::Config(game_version);
|
2021-03-07 23:41:21 -05:00
|
|
|
}
|
2023-01-07 11:24:02 -05:00
|
|
|
} // namespace REPL
|