// terminal_panel.cpp — render + process_output + shared logic. // Los backends (open/close/send) viven en terminal_panel_linux.cpp // y terminal_panel_windows.cpp respectivamente. #include "viz/terminal_panel/terminal_panel.h" #include "core/logger.h" #include "core/tokens.h" #include "imgui.h" #include #include #include namespace fn_term { namespace { // Convierte índice de color fn_term (0-16) a ImU32 RGBA para ImGui. // Usa la paleta kPalette16; fg=16 (default) → color de texto del tema ImGui. ImU32 color_to_imu32(uint8_t idx, bool is_fg) { if (idx == kColorDefault) { // Usar color del tema: FG → Text, BG → transparente. if (is_fg) return ImGui::GetColorU32(ImGuiCol_Text); return IM_COL32(0, 0, 0, 0); // transparente } // kPalette16 está en formato ABGR (little-endian), ImU32 también es ABGR en ImGui. return static_cast(kPalette16[idx]); } // Renderiza una línea del scrollback con colores. // Toma la línea como vector y escribe chunks de mismo color. void render_line(const TermLine& line) { if (line.empty()) { ImGui::NewLine(); return; } // Agrupar celdas consecutivas con mismo fg/bg/bold y emitir como texto. // Usamos un buffer temporal de la pila para evitar alloacs por línea. static char buf[4096]; size_t i = 0; while (i < line.size()) { uint8_t fg = line[i].fg; uint8_t bg = line[i].bg; // uint8_t bold = line[i].bold; // TODO(0132): bold rendering v2 // Acumular chars con mismo estilo. size_t j = i; int pos = 0; while (j < line.size() && line[j].fg == fg && line[j].bg == bg) { char32_t ch = line[j].ch; if (ch >= 0x20 && ch < 0x7F && pos < (int)sizeof(buf) - 2) { buf[pos++] = static_cast(ch); } else if (ch != U' ' && pos < (int)sizeof(buf) - 2) { buf[pos++] = '?'; // no-ASCII en v1 } else if (pos < (int)sizeof(buf) - 2) { buf[pos++] = ' '; } j++; } buf[pos] = '\0'; // Push color FG. ImU32 fg_col = color_to_imu32(fg, true); bool has_fg = (fg != kColorDefault); if (has_fg) ImGui::PushStyleColor(ImGuiCol_Text, fg_col); // Fondo: si BG definido, usar InvisibleButton + DrawList rect antes del texto. // En v1 simplificamos: solo coloreamos el texto (FG). BG requiere DrawList. // TODO(0132): renderizar celdas BG con InvisibleButton + DrawList en v2. ImGui::TextUnformatted(buf, buf + pos); if (has_fg) ImGui::PopStyleColor(); // Continuar en la misma línea si hay más celdas. if (j < line.size()) ImGui::SameLine(0.0f, 0.0f); i = j; } } } // namespace TerminalPanel::TerminalPanel() { // Reservar una línea inicial vacía. lines.emplace_back(); } TerminalPanel::~TerminalPanel() { if (is_open()) close(*this); } // --------------------------------------------------------------------------- // process_output — llamado desde el reader thread. // Parsea los bytes via AnsiParser y actualiza el scrollback buffer. // --------------------------------------------------------------------------- void process_output(TerminalPanel& panel, const char* data, size_t n) { std::lock_guard lk(panel.buf_mutex); panel.parser.feed(data, n, [&](const AnsiEvent& ev) { switch (ev.type) { case AnsiEventType::Char: { // Asegurar que tenemos al menos cur_row+1 filas. while ((int)panel.lines.size() <= panel.cur_row) panel.lines.emplace_back(); TermLine& line = panel.lines[panel.cur_row]; // Asegurar que la fila tiene al menos cur_col+1 celdas. while ((int)line.size() <= panel.cur_col) line.push_back(AnsiCell{}); line[panel.cur_col] = ev.cell; panel.cur_col++; break; } case AnsiEventType::Newline: { panel.cur_row++; // Scrollback circular: si excede el límite, eliminar la primera fila. while ((int)panel.lines.size() <= panel.cur_row) panel.lines.emplace_back(); if ((int)panel.lines.size() > panel.scrollback_lines) { int excess = (int)panel.lines.size() - panel.scrollback_lines; panel.lines.erase(panel.lines.begin(), panel.lines.begin() + excess); panel.cur_row -= excess; if (panel.cur_row < 0) panel.cur_row = 0; } panel.scroll_to_bottom = true; break; } case AnsiEventType::CarriageReturn: { panel.cur_col = 0; break; } case AnsiEventType::Backspace: { if (panel.cur_col > 0) panel.cur_col--; break; } case AnsiEventType::CursorAbsolute: { panel.cur_row = std::max(0, ev.cursor_abs.row); panel.cur_col = std::max(0, ev.cursor_abs.col); // Extender líneas si necesario. while ((int)panel.lines.size() <= panel.cur_row) panel.lines.emplace_back(); break; } case AnsiEventType::CursorMove: { switch (ev.cursor_rel.dir) { case CursorDir::Up: panel.cur_row = std::max(0, panel.cur_row - ev.cursor_rel.n); break; case CursorDir::Down: panel.cur_row += ev.cursor_rel.n; while ((int)panel.lines.size() <= panel.cur_row) panel.lines.emplace_back(); break; case CursorDir::Forward: panel.cur_col += ev.cursor_rel.n; break; case CursorDir::Back: panel.cur_col = std::max(0, panel.cur_col - ev.cursor_rel.n); break; } break; } case AnsiEventType::EraseDisplay: { panel.lines.clear(); panel.lines.emplace_back(); panel.cur_row = 0; panel.cur_col = 0; panel.parser.reset(); break; } case AnsiEventType::EraseLine: { while ((int)panel.lines.size() <= panel.cur_row) panel.lines.emplace_back(); panel.lines[panel.cur_row].clear(); panel.cur_col = 0; break; } } }); } // --------------------------------------------------------------------------- // render — debe llamarse dentro de un frame ImGui activo. // --------------------------------------------------------------------------- void render(TerminalPanel& panel) { // --- Toolbar --- ImGui::PushID("##term_toolbar"); if (ImGui::SmallButton("Clear")) { std::lock_guard lk(panel.buf_mutex); panel.lines.clear(); panel.lines.emplace_back(); panel.cur_row = 0; panel.cur_col = 0; } ImGui::SameLine(); if (ImGui::SmallButton("Copy")) { // Copiar todo el scrollback como texto plano al portapapeles. std::string text; std::lock_guard lk(panel.buf_mutex); for (const auto& line : panel.lines) { for (const auto& cell : line) { if (cell.ch >= 0x20 && cell.ch < 0x7F) text += static_cast(cell.ch); else if (cell.ch != U' ') text += '?'; else text += ' '; } text += '\n'; } ImGui::SetClipboardText(text.c_str()); } ImGui::SameLine(); if (ImGui::SmallButton("Reset") && panel.is_open()) { fn_term::close(panel); fn_term::open(panel); } ImGui::SameLine(); bool lock = !panel.scroll_to_bottom; if (ImGui::Checkbox("Lock scroll", &lock)) { panel.scroll_to_bottom = !lock; } ImGui::SameLine(); // Indicador de estado del proceso. if (!panel.is_open()) { ImGui::TextDisabled("[closed]"); } else if (panel.process_exited.load()) { ImGui::TextDisabled("[exited %d]", panel.exit_code); } else { ImGui::TextDisabled("[running]"); } ImGui::PopID(); // --- Scrollback area — fondo negro con texto gris claro --- ImVec2 avail = ImGui::GetContentRegionAvail(); // Reservar hueco para el input prompt si no es readonly. // GetFrameHeightWithSpacing() cubre una línea de InputText + padding. const float input_reserve = (!panel.readonly) ? (ImGui::GetFrameHeightWithSpacing() + 6.0f) : 0.0f; float child_h = std::max(avail.y - input_reserve, 32.0f); // Estilos del area terminal: fondo casi negro + texto gris claro. ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(10, 10, 10, 255)); ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(220, 220, 220, 255)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.0f, 6.0f)); ImGui::BeginChild("##term_scroll", ImVec2(0, child_h), ImGuiChildFlags_Borders, ImGuiWindowFlags_HorizontalScrollbar); { std::lock_guard lk(panel.buf_mutex); // Usar un clipper para evitar renderizar líneas fuera de vista. ImGuiListClipper clipper; clipper.Begin((int)panel.lines.size()); while (clipper.Step()) { for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) { render_line(panel.lines[i]); } } clipper.End(); } if (panel.scroll_to_bottom && ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 4.0f) { ImGui::SetScrollHereY(1.0f); } ImGui::EndChild(); ImGui::PopStyleVar(); // WindowPadding ImGui::PopStyleColor(2); // ChildBg + Text // --- Input prompt (visible siempre que readonly=false) --- if (!panel.readonly) { // Mostrar un prefijo "$ " antes del input box. ImGui::TextUnformatted("$ "); ImGui::SameLine(0.0f, 4.0f); static char s_input[1024] = {}; ImGui::SetNextItemWidth(-1.0f); // Si el shell está cerrado, desactivar el input. if (!panel.is_open()) ImGui::BeginDisabled(); bool enter = ImGui::InputText("##term_input", s_input, sizeof(s_input), ImGuiInputTextFlags_EnterReturnsTrue); if (!panel.is_open()) ImGui::EndDisabled(); if (enter && panel.is_open()) { std::string cmd = std::string(s_input) + "\n"; fn_term::send(panel, cmd); s_input[0] = '\0'; ImGui::SetKeyboardFocusHere(-1); } } } } // namespace fn_term