#pragma once #include "imgui.h" #include #include #include #include class TerminalPanel { public: static std::string buildCommandLine(const std::string& cwd, const std::string& command) { std::string workingDir = cwd.empty() ? "." : cwd; #ifdef _WIN32 return "cmd /C \"cd /d \\\"" + escapeForCmd(workingDir) + "\\\" && " + command + " 2>&1\""; #else return "sh -c 'cd \"" + escapeForSh(workingDir) + "\" && " + command + " 2>&1'"; #endif } void render(const std::string& cwd, ImFont* monoFont) { ImGui::PushFont(monoFont ? monoFont : ImGui::GetFont()); ImGui::PushItemWidth(-80.0f); bool runNow = inputText("##terminalCmd", commandBuf_, ImGuiInputTextFlags_EnterReturnsTrue); ImGui::PopItemWidth(); ImGui::SameLine(); if (ImGui::Button("Run") || runNow) { runCommand(cwd); } ImGui::SameLine(); if (ImGui::Button("Clear")) { output_.clear(); } ImGui::Separator(); ImGui::BeginChild("##terminalOutput", ImVec2(0, 0), false); if (output_.empty()) { ImGui::TextDisabled("(no output)"); } else { renderAnsiText(output_); if (autoScroll_ && ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 20.0f) { ImGui::SetScrollHereY(1.0f); } } ImGui::EndChild(); ImGui::PopFont(); } void append(const std::string& text) { output_ += text; } int runAndAppend(const std::string& cwd, const std::string& command) { std::string out; int code = runProcess(cwd, command, out); output_ += "> " + command + "\n"; output_ += out; return code; } int runAndCapture(const std::string& cwd, const std::string& command, std::string& out) { out.clear(); int code = runProcess(cwd, command, out); output_ += "> " + command + "\n"; output_ += out; return code; } const std::string& getOutput() const { return output_; } private: std::string commandBuf_; std::string output_; bool autoScroll_ = true; struct InputTextCallbackData { std::string* str; }; static int inputTextCallback(ImGuiInputTextCallbackData* data) { auto* userData = static_cast(data->UserData); if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) { userData->str->resize(data->BufTextLen); data->Buf = userData->str->data(); } return 0; } static bool inputText(const char* label, std::string& str, ImGuiInputTextFlags flags) { flags |= ImGuiInputTextFlags_CallbackResize; InputTextCallbackData cbData{&str}; if (str.capacity() < str.size() + 64) str.reserve(str.size() + 256); return ImGui::InputText(label, str.data(), str.capacity() + 1, flags, inputTextCallback, &cbData); } static std::string escapeForCmd(const std::string& s) { std::string out; out.reserve(s.size()); for (char c : s) { if (c == '"') out += "\\\""; else out += c; } return out; } static std::string escapeForSh(const std::string& s) { std::string out; out.reserve(s.size()); for (char c : s) { if (c == '\'') out += "'\\''"; else out += c; } return out; } void runCommand(const std::string& cwd) { if (commandBuf_.empty()) return; std::string out; int code = runProcess(cwd, commandBuf_, out); output_ += "> " + commandBuf_ + "\n"; output_ += out; output_ += "[exit code: " + std::to_string(code) + "]\n"; } static int runProcess(const std::string& cwd, const std::string& command, std::string& out) { std::string fullCmd = buildCommandLine(cwd, command); #ifdef _WIN32 FILE* pipe = _popen(fullCmd.c_str(), "r"); #else FILE* pipe = popen(fullCmd.c_str(), "r"); #endif if (!pipe) { out += "[terminal] Failed to launch command.\n"; return -1; } char buffer[4096]; while (!feof(pipe)) { if (fgets(buffer, sizeof(buffer), pipe) != nullptr) { out += buffer; } } #ifdef _WIN32 int code = _pclose(pipe); #else int code = pclose(pipe); #endif return code; } static ImVec4 ansiColor(int code, const ImVec4& base) { switch (code) { case 30: return ImVec4(0.0f, 0.0f, 0.0f, 1.0f); case 31: return ImVec4(0.85f, 0.2f, 0.2f, 1.0f); case 32: return ImVec4(0.2f, 0.75f, 0.3f, 1.0f); case 33: return ImVec4(0.85f, 0.65f, 0.2f, 1.0f); case 34: return ImVec4(0.35f, 0.55f, 0.95f, 1.0f); case 35: return ImVec4(0.8f, 0.35f, 0.8f, 1.0f); case 36: return ImVec4(0.2f, 0.75f, 0.75f, 1.0f); case 37: return ImVec4(0.8f, 0.8f, 0.8f, 1.0f); case 90: return ImVec4(0.45f, 0.45f, 0.45f, 1.0f); case 91: return ImVec4(1.0f, 0.4f, 0.4f, 1.0f); case 92: return ImVec4(0.4f, 1.0f, 0.4f, 1.0f); case 93: return ImVec4(1.0f, 0.9f, 0.4f, 1.0f); case 94: return ImVec4(0.5f, 0.7f, 1.0f, 1.0f); case 95: return ImVec4(1.0f, 0.6f, 1.0f, 1.0f); case 96: return ImVec4(0.5f, 1.0f, 1.0f, 1.0f); case 97: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); case 39: return base; default: return base; } } static void renderAnsiText(const std::string& text) { ImVec4 baseColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); ImVec4 currentColor = baseColor; std::string buffer; bool lineStart = true; auto emit = [&](const std::string& chunk) { if (chunk.empty()) return; if (!lineStart) ImGui::SameLine(0.0f, 0.0f); ImGui::TextColored(currentColor, "%s", chunk.c_str()); lineStart = false; }; for (size_t i = 0; i < text.size(); ) { if (text[i] == '\x1b' && i + 1 < text.size() && text[i + 1] == '[') { emit(buffer); buffer.clear(); i += 2; std::string codeStr; while (i < text.size() && text[i] != 'm') { codeStr += text[i++]; } if (i < text.size() && text[i] == 'm') i++; if (codeStr.empty()) { currentColor = baseColor; continue; } std::stringstream ss(codeStr); std::string part; while (std::getline(ss, part, ';')) { int code = 0; try { code = std::stoi(part); } catch (...) { code = 0; } if (code == 0) { currentColor = baseColor; } else { currentColor = ansiColor(code, currentColor); } } continue; } if (text[i] == '\n') { emit(buffer); buffer.clear(); ImGui::NewLine(); lineStart = true; ++i; continue; } buffer.push_back(text[i++]); } emit(buffer); } };