#pragma once #include "imgui.h" #include "EmacsIntegration.h" #include "NotificationSystem.h" #include "StringUtils.h" #include #include #include #include #include struct EmacsPackageEntry { std::string name; std::string version; std::string description; std::string status; // loaded | available | not-installed }; struct EmacsPackageBrowserState { std::vector packages; char filterBuf[128] = {}; char loadBuf[128] = {}; int selected = -1; bool showAvailable = true; bool needsRefresh = true; std::string lastError; }; // trimCopy is provided by EditorUtils.h static inline std::string toLowerCopyPkg(const std::string& s) { std::string out = s; std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) { return (char)std::tolower(c); }); return out; } static std::string buildPackageListCommand(const std::string& listSymbol) { std::string cmd = "(progn " "(require 'package) " "(mapconcat " " (lambda (p) " " (let* ((desc (cond " " ((and (consp (cdr p)) (listp (cdr p))) (car (cdr p))) " " ((consp (cdr p)) (cdr p)) " " (t nil))) " " (name (symbol-name (car p))) " " (ver (if (and desc (fboundp 'package-desc-version)) " " (package-version-join (package-desc-version desc)) \"\")) " " (summary (if (and desc (fboundp 'package-desc-summary)) " " (package-desc-summary desc) \"\"))) " " (concat name \"|\" ver \"|\" summary))) " + listSymbol + " \"\\n\"))"; return cmd; } static std::vector parsePackageListText(const std::string& text) { std::vector out; if (text.empty()) return out; size_t start = 0; while (start < text.size()) { size_t end = text.find('\n', start); if (end == std::string::npos) end = text.size(); std::string line = trimCopy(text.substr(start, end - start)); if (!line.empty()) { size_t a = line.find('|'); size_t b = a == std::string::npos ? std::string::npos : line.find('|', a + 1); EmacsPackageEntry entry; if (a == std::string::npos) { entry.name = line; } else { entry.name = trimCopy(line.substr(0, a)); if (b == std::string::npos) { entry.version = trimCopy(line.substr(a + 1)); } else { entry.version = trimCopy(line.substr(a + 1, b - a - 1)); entry.description = trimCopy(line.substr(b + 1)); } } if (!entry.name.empty()) { out.push_back(std::move(entry)); } } start = end + 1; } return out; } static std::vector mergePackageLists(const std::vector& loaded, const std::vector& available, bool includeAvailable) { std::unordered_map merged; for (auto entry : loaded) { entry.status = "loaded"; merged[entry.name] = std::move(entry); } if (includeAvailable) { for (auto entry : available) { auto it = merged.find(entry.name); if (it == merged.end()) { entry.status = "available"; merged[entry.name] = std::move(entry); } else if (it->second.description.empty() && !entry.description.empty()) { it->second.description = entry.description; } } } std::vector out; out.reserve(merged.size()); for (auto& kv : merged) out.push_back(std::move(kv.second)); std::sort(out.begin(), out.end(), [](const EmacsPackageEntry& a, const EmacsPackageEntry& b) { return a.name < b.name; }); return out; } static std::vector queryEmacsPackageList(EmacsConnection& emacs, bool includeAvailable, NotificationSystem& notifications, std::string& outError) { outError.clear(); std::string loadedText = emacs.sendCommand(buildPackageListCommand("package-alist")); if (loadedText.empty() && !emacs.getLastError().empty()) { outError = emacs.getLastError(); notifications.notify(NotificationLevel::Error, "[emacs] " + outError); } auto loaded = parsePackageListText(loadedText); std::vector available; if (includeAvailable) { std::string availText = emacs.sendCommand(buildPackageListCommand("package-archive-contents")); if (availText.empty() && !emacs.getLastError().empty()) { outError = emacs.getLastError(); notifications.notify(NotificationLevel::Error, "[emacs] " + outError); } available = parsePackageListText(availText); } return mergePackageLists(loaded, available, includeAvailable); } static void refreshEmacsPackages(EmacsPackageBrowserState& state, EmacsConnection& emacs, NotificationSystem& notifications) { state.lastError.clear(); state.packages = queryEmacsPackageList(emacs, state.showAvailable, notifications, state.lastError); state.needsRefresh = false; } static bool loadEmacsPackage(EmacsConnection& emacs, const std::string& packageName, NotificationSystem& notifications) { if (packageName.empty()) return false; std::string cmd = "(require '" + packageName + ")"; std::string result = emacs.sendCommand(cmd); if (!emacs.getLastError().empty() && result == "error") { notifications.notify(NotificationLevel::Error, "[emacs] Failed to load " + packageName + ": " + emacs.getLastError()); return false; } notifications.notify(NotificationLevel::Success, "[emacs] Loaded package: " + packageName); return true; } static bool emacsPackageMatchesFilter(const EmacsPackageEntry& entry, const std::string& filter) { if (filter.empty()) return true; std::string hay = toLowerCopyPkg(entry.name + " " + entry.description); std::string needle = toLowerCopyPkg(filter); return hay.find(needle) != std::string::npos; } static bool renderEmacsPackageBrowser(EmacsPackageBrowserState& state, EmacsConnection& emacs, NotificationSystem& notifications) { bool updated = false; if (ImGui::Button("Refresh")) { state.needsRefresh = true; } ImGui::SameLine(); if (ImGui::Checkbox("Show Available", &state.showAvailable)) { state.needsRefresh = true; } ImGui::SameLine(); ImGui::InputText("Filter", state.filterBuf, sizeof(state.filterBuf)); if (state.needsRefresh) { refreshEmacsPackages(state, emacs, notifications); updated = true; } ImGui::Separator(); if (state.packages.empty()) { ImGui::TextDisabled("(no packages loaded)"); } else { if (ImGui::BeginTable("##emacsPkgs", 4, ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders)) { ImGui::TableSetupColumn("Package", ImGuiTableColumnFlags_WidthStretch, 0.25f); ImGui::TableSetupColumn("Version", ImGuiTableColumnFlags_WidthStretch, 0.15f); ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 0.15f); ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch, 0.45f); ImGui::TableHeadersRow(); std::string filter = state.filterBuf; int row = 0; for (int i = 0; i < (int)state.packages.size(); ++i) { const auto& entry = state.packages[i]; if (!emacsPackageMatchesFilter(entry, filter)) continue; ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); bool selected = (state.selected == i); if (ImGui::Selectable(entry.name.c_str(), selected, ImGuiSelectableFlags_SpanAllColumns)) { state.selected = i; } ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(entry.version.empty() ? "-" : entry.version.c_str()); ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(entry.status.empty() ? "-" : entry.status.c_str()); ImGui::TableSetColumnIndex(3); ImGui::TextWrapped("%s", entry.description.empty() ? "-" : entry.description.c_str()); ++row; } if (row == 0) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("(no matches)"); } ImGui::EndTable(); } } ImGui::Separator(); ImGui::InputText("Load Package", state.loadBuf, sizeof(state.loadBuf)); ImGui::SameLine(); std::string loadTarget = state.loadBuf; if (loadTarget.empty() && state.selected >= 0 && state.selected < (int)state.packages.size()) { loadTarget = state.packages[state.selected].name; } if (ImGui::Button("Load")) { if (loadTarget.empty()) { notifications.notify(NotificationLevel::Warning, "[emacs] No package selected."); } else { bool ok = loadEmacsPackage(emacs, loadTarget, notifications); if (!ok) { notifications.notify(NotificationLevel::Warning, "[emacs] Package not installed: " + loadTarget); } state.needsRefresh = true; updated = true; } } if (!loadTarget.empty()) { ImGui::SameLine(); ImGui::TextDisabled("%s", loadTarget.c_str()); } return updated; }