feat(0132): add terminal_panel demo (oneshot, interactive, readonly)

Add demos_terminal.cpp con 3 demos del modulo terminal_panel:
- Demo 1: one-shot spawn bash, echo hello; date; ls /tmp
- Demo 2: terminal interactivo bash/cmd.exe con input box
- Demo 3: readonly tail -f con boton append

Actualiza CMakeLists.txt con ansi_parser + terminal_panel + -lutil (Linux).
Actualiza demos.h + main.cpp con entry "Terminal" en el sidebar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 23:34:56 +02:00
parent 61d21cc1ff
commit 9979d74217
4 changed files with 203 additions and 0 deletions
+11
View File
@@ -91,6 +91,12 @@ add_imgui_app(primitives_gallery
${CMAKE_SOURCE_DIR}/functions/gfx/mesh_gpu.cpp ${CMAKE_SOURCE_DIR}/functions/gfx/mesh_gpu.cpp
${CMAKE_SOURCE_DIR}/functions/core/orbit_camera.cpp ${CMAKE_SOURCE_DIR}/functions/core/orbit_camera.cpp
${CMAKE_SOURCE_DIR}/functions/viz/mesh_viewer.cpp ${CMAKE_SOURCE_DIR}/functions/viz/mesh_viewer.cpp
# terminal_panel module (issue 0132)
demos_terminal.cpp
${CMAKE_SOURCE_DIR}/functions/core/ansi_parser.cpp
${CMAKE_SOURCE_DIR}/functions/viz/terminal_panel/terminal_panel.cpp
${CMAKE_SOURCE_DIR}/functions/viz/terminal_panel/terminal_panel_linux.cpp
${CMAKE_SOURCE_DIR}/functions/viz/terminal_panel/terminal_panel_windows.cpp
) )
target_include_directories(primitives_gallery PRIVATE target_include_directories(primitives_gallery PRIVATE
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit ${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit
@@ -108,3 +114,8 @@ endif()
if(WIN32) if(WIN32)
set_target_properties(primitives_gallery PROPERTIES WIN32_EXECUTABLE TRUE) set_target_properties(primitives_gallery PROPERTIES WIN32_EXECUTABLE TRUE)
endif() endif()
# terminal_panel: forkpty requiere -lutil en Linux.
if(NOT WIN32)
target_link_libraries(primitives_gallery PRIVATE util)
endif()
+3
View File
@@ -53,4 +53,7 @@ void demo_shader_canvas();
void demo_gl_texture(); // wave 1, issue 0026 void demo_gl_texture(); // wave 1, issue 0026
void demo_gl_info(); // issue 0049b — runtime GL version + 4.3 caps void demo_gl_info(); // issue 0049b — runtime GL version + 4.3 caps
// --- Terminal (issue 0132) ---
void demo_terminal();
} // namespace gallery } // namespace gallery
+187
View File
@@ -0,0 +1,187 @@
// demos_terminal.cpp — demos del modulo terminal_panel (issue 0132).
//
// Demo 1: One-shot — spawn bash que ejecuta "echo hello; date; ls /tmp | head"
// con auto-close cuando el proceso termina. Muestra el scrollback.
// Demo 2: Interactivo — bash -i (Linux) / cmd.exe (Windows) con input box.
// Demo 3: Readonly — tail simulado de un archivo de log.
#include "demos.h"
#include "demo.h"
#include "viz/terminal_panel/terminal_panel.h"
#include "imgui.h"
#include <cstring>
#include <string>
namespace gallery {
// ---------------------------------------------------------------------------
// Demo 1: One-shot con auto-close
// ---------------------------------------------------------------------------
void demo_terminal_oneshot() {
demo_header("terminal_panel", "v1.0.0",
"Emulador TTY embebible. Demo 1: one-shot — spawn bash, ejecuta comandos, auto-close.");
static fn_term::TerminalPanel s_term;
static bool s_started = false;
if (!s_started) {
ImGui::TextUnformatted("Pulsa 'Run' para lanzar el one-shot.");
if (ImGui::Button("Run")) {
if (!s_term.is_open()) {
s_term.shell = "/bin/bash";
s_term.scrollback_lines = 200;
s_term.readonly = true; // solo output, sin input
fn_term::open(s_term);
fn_term::send(s_term, "echo hello; date; ls /tmp | head -5; exit 0\n");
s_started = true;
}
}
return;
}
fn_term::render(s_term);
if (s_term.process_exited.load() && s_term.is_open()) {
ImGui::SameLine();
ImGui::TextDisabled("[done — exit %d]", s_term.exit_code);
}
if (ImGui::Button("Reset demo")) {
if (s_term.is_open()) fn_term::close(s_term);
s_started = false;
}
code_block(
"fn_term::TerminalPanel term;\n"
"term.shell = \"/bin/bash\";\n"
"term.readonly = true;\n"
"fn_term::open(term);\n"
"fn_term::send(term, \"echo hello; date; exit 0\\n\");\n"
"// En cada frame ImGui:\n"
"fn_term::render(term);\n"
"if (term.process_exited.load()) fn_term::close(term);"
);
}
// ---------------------------------------------------------------------------
// Demo 2: Interactivo
// ---------------------------------------------------------------------------
void demo_terminal_interactive() {
demo_header("terminal_panel", "v1.0.0",
"Demo 2: terminal interactivo — bash -i (Linux) / cmd.exe (Windows).");
static fn_term::TerminalPanel s_term;
if (!s_term.is_open()) {
if (ImGui::Button("Open shell")) {
#ifdef _WIN32
s_term.shell = "cmd.exe";
#else
s_term.shell = "/bin/bash";
#endif
s_term.scrollback_lines = 1000;
s_term.readonly = false;
fn_term::open(s_term);
}
return;
}
fn_term::render(s_term);
if (ImGui::Button("Close shell")) {
fn_term::close(s_term);
}
}
// ---------------------------------------------------------------------------
// Demo 3: Readonly tail
// ---------------------------------------------------------------------------
void demo_terminal_readonly() {
demo_header("terminal_panel", "v1.0.0",
"Demo 3: readonly — tail de /tmp/fn_gallery_test.log (creado al abrir).");
static fn_term::TerminalPanel s_term;
static bool s_log_created = false;
// Crear el archivo de log de prueba si no existe.
if (!s_log_created) {
if (ImGui::Button("Start tail")) {
// Escribir unas líneas de muestra al log.
FILE* f = fopen("/tmp/fn_gallery_test.log", "w");
if (f) {
for (int i = 0; i < 5; i++)
fprintf(f, "[sample] log line %d\n", i);
fclose(f);
}
s_term.shell = "/bin/bash";
s_term.readonly = true;
s_term.scrollback_lines = 500;
fn_term::open(s_term);
fn_term::send(s_term, "tail -f /tmp/fn_gallery_test.log\n");
s_log_created = true;
}
return;
}
fn_term::render(s_term);
ImGui::Separator();
if (ImGui::Button("Append line")) {
FILE* f = fopen("/tmp/fn_gallery_test.log", "a");
if (f) {
static int s_count = 0;
fprintf(f, "[appended] line %d\n", s_count++);
fclose(f);
}
}
ImGui::SameLine();
if (ImGui::Button("Stop")) {
if (s_term.is_open()) fn_term::close(s_term);
s_log_created = false;
}
code_block(
"fn_term::TerminalPanel term;\n"
"term.shell = \"/bin/bash\";\n"
"term.readonly = true; // sin input box\n"
"fn_term::open(term);\n"
"fn_term::send(term, \"tail -f /tmp/agent.log\\n\");\n"
"// En cada frame:\n"
"fn_term::render(term);"
);
}
// ---------------------------------------------------------------------------
// Entry point unificado (llamado desde main.cpp via demos.h)
// ---------------------------------------------------------------------------
void demo_terminal() {
static int s_tab = 0;
if (ImGui::BeginTabBar("##term_tabs")) {
if (ImGui::BeginTabItem("One-shot")) {
s_tab = 0;
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Interactive")) {
s_tab = 1;
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Readonly tail")) {
s_tab = 2;
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
switch (s_tab) {
case 0: demo_terminal_oneshot(); break;
case 1: demo_terminal_interactive(); break;
case 2: demo_terminal_readonly(); break;
}
}
} // namespace gallery
+2
View File
@@ -81,6 +81,8 @@ static const DemoEntry k_demos[] = {
{"shader_canvas", "shader_canvas", "Gfx", &gallery::demo_shader_canvas}, {"shader_canvas", "shader_canvas", "Gfx", &gallery::demo_shader_canvas},
{"gl_texture", "gl_texture_load", "Gfx", &gallery::demo_gl_texture}, // wave 1 {"gl_texture", "gl_texture_load", "Gfx", &gallery::demo_gl_texture}, // wave 1
{"gl_info", "gl_info", "Gfx", &gallery::demo_gl_info}, // issue 0049b {"gl_info", "gl_info", "Gfx", &gallery::demo_gl_info}, // issue 0049b
// Terminal (issue 0132)
{"terminal_panel", "terminal_panel", "Terminal", &gallery::demo_terminal},
}; };
static constexpr int k_demo_count = sizeof(k_demos) / sizeof(k_demos[0]); static constexpr int k_demo_count = sizeof(k_demos) / sizeof(k_demos[0]);