diff --git a/CMakeLists.txt b/CMakeLists.txt index c71a213..dd08e23 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,12 @@ add_imgui_app(primitives_gallery ${CMAKE_SOURCE_DIR}/functions/gfx/mesh_gpu.cpp ${CMAKE_SOURCE_DIR}/functions/core/orbit_camera.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 ${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit @@ -108,3 +114,8 @@ endif() if(WIN32) set_target_properties(primitives_gallery PROPERTIES WIN32_EXECUTABLE TRUE) endif() + +# terminal_panel: forkpty requiere -lutil en Linux. +if(NOT WIN32) + target_link_libraries(primitives_gallery PRIVATE util) +endif() diff --git a/demos.h b/demos.h index 8e460f3..348a532 100644 --- a/demos.h +++ b/demos.h @@ -53,4 +53,7 @@ void demo_shader_canvas(); void demo_gl_texture(); // wave 1, issue 0026 void demo_gl_info(); // issue 0049b — runtime GL version + 4.3 caps +// --- Terminal (issue 0132) --- +void demo_terminal(); + } // namespace gallery diff --git a/demos_terminal.cpp b/demos_terminal.cpp new file mode 100644 index 0000000..370211f --- /dev/null +++ b/demos_terminal.cpp @@ -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 +#include + +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 diff --git a/main.cpp b/main.cpp index 22c76f6..f423429 100644 --- a/main.cpp +++ b/main.cpp @@ -81,6 +81,8 @@ static const DemoEntry k_demos[] = { {"shader_canvas", "shader_canvas", "Gfx", &gallery::demo_shader_canvas}, {"gl_texture", "gl_texture_load", "Gfx", &gallery::demo_gl_texture}, // wave 1 {"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]);