16
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-07-18 14:39:18 +02:00
Files
maszyna/DevConsole/devconsole.cpp
2026-05-24 21:48:05 +02:00

380 lines
12 KiB
C++

/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "DevConsole/devconsole.h"
#include <sstream>
#include <algorithm>
#include <cctype>
#include "utilities/Globals.h"
#include "utilities/Logs.h"
namespace dev {
// -----------------------------------------------------------------------
// Global singleton
// -----------------------------------------------------------------------
console_panel Console;
// -----------------------------------------------------------------------
// helpers
// -----------------------------------------------------------------------
static std::string str_tolower(std::string s)
{
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return std::tolower(c); });
return s;
}
// Tokenise a command line. Handles "quoted tokens" as a single arg.
static args_t tokenise(const std::string &line)
{
args_t tokens;
std::istringstream ss(line);
std::string tok;
while (ss >> std::quoted(tok))
tokens.push_back(tok);
return tokens;
}
// -----------------------------------------------------------------------
// console_panel
// -----------------------------------------------------------------------
console_panel::console_panel()
: ui_panel("DevConsole", false)
{
title = "Developer Console";
register_builtins();
}
void console_panel::register_builtins()
{
register_command("help", "List all commands, or describe one: help [command]",
[this](const args_t &args) {
if (args.size() >= 2) {
auto it = m_commands.find(str_tolower(args[1]));
if (it == m_commands.end()) {
print_error("Unknown command: " + args[1]);
} else {
print_ok(it->second.name + "" + it->second.description);
}
return;
}
print_info("Available commands:");
std::vector<std::string> names;
names.reserve(m_commands.size());
for (auto &kv : m_commands)
names.push_back(kv.first);
std::sort(names.begin(), names.end());
for (auto &n : names) {
std::string line = " " + n;
auto desc = m_commands[n].description;
if (!desc.empty())
line += "" + desc;
print(line);
}
});
register_command("clear", "Clear the console output",
[this](const args_t &) {
m_log.clear();
});
register_command("ping", "Responds with pong",
[this](const args_t &) {
print_ok("pong");
});
register_command("echo", "Print arguments back: echo [text...]",
[this](const args_t &args) {
std::string msg;
for (std::size_t i = 1; i < args.size(); ++i) {
if (i > 1) msg += ' ';
msg += args[i];
}
print(msg);
});
register_command("history", "Show command history",
[this](const args_t &) {
if (m_history.empty()) {
print_info("(history is empty)");
return;
}
int idx = 0;
for (auto &h : m_history)
print(" " + std::to_string(idx++) + ": " + h);
});
}
// -----------------------------------------------------------------------
// Public API
// -----------------------------------------------------------------------
void console_panel::register_command(std::string name, std::string description,
command_handler_t handler)
{
std::string key = str_tolower(name);
m_commands[key] = console_command{ std::move(name), std::move(description),
std::move(handler) };
}
void console_panel::print(const std::string &text, glm::vec4 color)
{
// Split multi-line strings into separate entries
std::istringstream ss(text);
std::string line;
while (std::getline(ss, line))
m_log.push_back({ line, color });
while (m_log.size() > MAX_LOG)
m_log.pop_front();
m_scroll_to_bottom = true;
}
void console_panel::print_info(const std::string &text)
{
print("[info] " + text, { 0.6f, 0.8f, 1.0f, 1.0f });
}
void console_panel::print_ok(const std::string &text)
{
print("[ok] " + text, { 0.42f, 0.80f, 0.23f, 1.0f });
}
void console_panel::print_error(const std::string &text)
{
print("[error] " + text, { 1.0f, 0.35f, 0.35f, 1.0f });
}
void console_panel::print_warn(const std::string &text)
{
print("[warn] " + text, { 1.0f, 0.80f, 0.20f, 1.0f });
}
void console_panel::execute(const std::string &input)
{
std::string trimmed = input;
// ltrim / rtrim
trimmed.erase(0, trimmed.find_first_not_of(" \t\r\n"));
trimmed.erase(trimmed.find_last_not_of(" \t\r\n") + 1);
if (trimmed.empty())
return;
// Echo the input
print("> " + trimmed, { 0.42f, 0.64f, 0.23f, 1.0f });
// Save to history (no duplicates at the end)
if (m_history.empty() || m_history.back() != trimmed)
m_history.push_back(trimmed);
if (m_history.size() > MAX_HIST)
m_history.pop_front();
m_history_pos = -1;
auto tokens = tokenise(trimmed);
if (tokens.empty())
return;
std::string cmd_name = str_tolower(tokens[0]);
execute_command(cmd_name, tokens);
}
void console_panel::execute_command(const std::string &name, const args_t &args)
{
auto it = m_commands.find(name);
if (it == m_commands.end()) {
print_error("Unknown command '" + name + "'. Type 'help' for a list.");
return;
}
try {
it->second.handler(args);
}
catch (const std::exception &e) {
print_error(std::string("Exception: ") + e.what());
}
}
std::vector<std::string> console_panel::completions_for(const std::string &prefix) const
{
std::string lp = str_tolower(prefix);
std::vector<std::string> result;
for (auto &kv : m_commands)
if (kv.first.substr(0, lp.size()) == lp)
result.push_back(kv.first);
std::sort(result.begin(), result.end());
return result;
}
// -----------------------------------------------------------------------
// ImGui InputText callback
// -----------------------------------------------------------------------
struct CallbackUserData {
console_panel *self;
// current text in the buffer at callback time
std::string completion_prefix;
};
int console_panel::input_callback(ImGuiInputTextCallbackData *data)
{
auto *ud = static_cast<CallbackUserData *>(data->UserData);
console_panel *self = ud->self;
if (data->EventFlag == ImGuiInputTextFlags_CallbackCompletion) {
// Collect the word being typed (last space-delimited token)
const char *end = data->Buf + data->CursorPos;
const char *begin = end;
while (begin > data->Buf && *(begin - 1) != ' ')
--begin;
std::string prefix(begin, end);
auto candidates = self->completions_for(prefix);
if (candidates.empty()) {
// nothing
} else if (candidates.size() == 1) {
// Replace the partial token with the full match + space
data->DeleteChars(static_cast<int>(begin - data->Buf),
static_cast<int>(end - begin));
data->InsertChars(data->CursorPos, candidates[0].c_str());
data->InsertChars(data->CursorPos, " ");
} else {
// Find common prefix among candidates
std::string common = candidates[0];
for (std::size_t i = 1; i < candidates.size(); ++i) {
std::size_t j = 0;
while (j < common.size() && j < candidates[i].size() &&
common[j] == candidates[i][j])
++j;
common.resize(j);
}
if (common.size() > prefix.size()) {
data->DeleteChars(static_cast<int>(begin - data->Buf),
static_cast<int>(end - begin));
data->InsertChars(data->CursorPos, common.c_str());
}
// Show all candidates
self->print_info("Candidates:");
for (auto &c : candidates)
self->print(" " + c);
}
}
else if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory) {
const int prev_pos = self->m_history_pos;
if (data->EventKey == ImGuiKey_UpArrow) {
if (self->m_history_pos == -1)
self->m_history_pos = static_cast<int>(self->m_history.size()) - 1;
else if (self->m_history_pos > 0)
--self->m_history_pos;
} else if (data->EventKey == ImGuiKey_DownArrow) {
if (self->m_history_pos != -1) {
++self->m_history_pos;
if (self->m_history_pos >= static_cast<int>(self->m_history.size()))
self->m_history_pos = -1;
}
}
if (prev_pos != self->m_history_pos) {
const char *entry = (self->m_history_pos >= 0)
? self->m_history[self->m_history_pos].c_str()
: "";
data->DeleteChars(0, data->BufTextLen);
data->InsertChars(0, entry);
}
}
return 0;
}
// -----------------------------------------------------------------------
// Rendering
// -----------------------------------------------------------------------
void console_panel::render()
{
if (!is_open)
return;
ImGui::SetNextWindowSize(ImVec2(700, 400), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSizeConstraints(ImVec2(400, 200), ImVec2(FLT_MAX, FLT_MAX));
auto const panelname = (title.empty() ? m_name : title) + "###" + m_name;
if (ImGui::Begin(panelname.c_str(), &is_open, ImGuiWindowFlags_NoCollapse))
render_contents();
ImGui::End();
}
void console_panel::render_contents()
{
// --- Log area ---
const float footer_height = ImGui::GetStyle().ItemSpacing.y
+ ImGui::GetFrameHeightWithSpacing();
ImGui::BeginChild("console_scrolling",
ImVec2(0, -footer_height),
false,
ImGuiWindowFlags_HorizontalScrollbar);
ImGui::PushFont(ui_layer::font_mono);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 1));
for (auto &entry : m_log) {
ImGui::PushStyleColor(ImGuiCol_Text,
ImVec4(entry.color.r, entry.color.g,
entry.color.b, entry.color.a));
ImGui::TextUnformatted(entry.text.c_str());
ImGui::PopStyleColor();
}
if (m_scroll_to_bottom ||
ImGui::GetScrollY() >= ImGui::GetScrollMaxY())
ImGui::SetScrollHereY(1.0f);
m_scroll_to_bottom = false;
ImGui::PopStyleVar();
ImGui::PopFont();
ImGui::EndChild();
ImGui::Separator();
// --- Input line ---
ImGui::PushFont(ui_layer::font_mono);
bool reclaim_focus = false;
ImGuiInputTextFlags input_flags =
ImGuiInputTextFlags_EnterReturnsTrue |
ImGuiInputTextFlags_CallbackCompletion |
ImGuiInputTextFlags_CallbackHistory;
CallbackUserData cbud{ this, "" };
ImGui::SetNextItemWidth(-1.0f);
if (ImGui::InputText("##console_input", m_input_buf, sizeof(m_input_buf),
input_flags, &console_panel::input_callback, &cbud))
{
execute(m_input_buf);
m_input_buf[0] = '\0';
reclaim_focus = true;
}
ImGui::PopFont();
// Auto-focus the input field when the window is shown
ImGui::SetItemDefaultFocus();
if (reclaim_focus || m_want_focus) {
ImGui::SetKeyboardFocusHere(-1);
m_want_focus = false;
}
}
} // namespace dev