/* 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 #include #include #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 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 console_panel::completions_for(const std::string &prefix) const { std::string lp = str_tolower(prefix); std::vector 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(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(begin - data->Buf), static_cast(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(begin - data->Buf), static_cast(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(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(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