Author: Nagesh Nazare Date: 2026-05-07T10:46:42-07:00 New Revision: 7578a9ad9361a6589fcfd89a36a24496cb6b5ec4
URL: https://github.com/llvm/llvm-project/commit/7578a9ad9361a6589fcfd89a36a24496cb6b5ec4 DIFF: https://github.com/llvm/llvm-project/commit/7578a9ad9361a6589fcfd89a36a24496cb6b5ec4.diff LOG: [lldb] Real-time console pane for output in lldb tui (#177160) Creating a feature branch. New Console Pane that - - captures stdout/stderr msgs from debugged process - displays output to console in real-time - provides scrolling and navigation features - manages 10K line circular buffer - auto-scroll on/off feature <img width="2940" height="1744" alt="image" src="https://github.com/user-attachments/assets/49e9dd3e-9f6f-4383-820b-ce365a46208f" /> Controls - enable / disable : F5 + o tab to switch to Console pane UP/ DOWN / PAGEUP/ PAGEDOWN for scrolling, HOME/ END for top or bottom c for clear output Tested on Mac Added: lldb/test/API/commands/gui/console-output/Makefile lldb/test/API/commands/gui/console-output/TestGuiConsoleOutput.py lldb/test/API/commands/gui/console-output/main.cpp Modified: lldb/include/lldb/Core/Debugger.h lldb/source/Core/CoreProperties.td lldb/source/Core/Debugger.cpp lldb/source/Core/IOHandlerCursesGUI.cpp llvm/docs/ReleaseNotes.md Removed: ################################################################################ diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h index e53e916d78cc1..82d86f988f07f 100644 --- a/lldb/include/lldb/Core/Debugger.h +++ b/lldb/include/lldb/Core/Debugger.h @@ -406,6 +406,8 @@ class Debugger : public std::enable_shared_from_this<Debugger>, bool SetShowInlineDiagnostics(bool); + uint64_t GetGuiMaxConsoleLines() const; + bool LoadPlugin(const FileSpec &spec, Status &error); void RunIOHandlers(); diff --git a/lldb/source/Core/CoreProperties.td b/lldb/source/Core/CoreProperties.td index 8cea0931868aa..e4a565e97b81f 100644 --- a/lldb/source/Core/CoreProperties.td +++ b/lldb/source/Core/CoreProperties.td @@ -293,4 +293,8 @@ let Definition = "debugger", Path = "" in { Global, DefaultFalse, Desc<"Controls whether diagnostics can refer directly to the command input, drawing arrows to it. If false, diagnostics will echo the input.">; + def GuiMaxConsoleLines: Property<"gui-console-max-lines", "UInt64">, + Global, + DefaultUnsignedValue<10000>, + Desc<"The maximum number of lines to keep in the console output window.">; } diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp index 48e03881fa3b5..e9fe71108c572 100644 --- a/lldb/source/Core/Debugger.cpp +++ b/lldb/source/Core/Debugger.cpp @@ -780,6 +780,12 @@ bool Debugger::SetShowInlineDiagnostics(bool b) { return SetPropertyAtIndex(idx, b); } +uint64_t Debugger::GetGuiMaxConsoleLines() const { + const uint32_t idx = ePropertyGuiMaxConsoleLines; + return GetPropertyAtIndexAs<uint64_t>( + idx, g_debugger_properties[idx].default_uint_value); +} + #pragma mark Debugger // const DebuggerPropertiesSP & diff --git a/lldb/source/Core/IOHandlerCursesGUI.cpp b/lldb/source/Core/IOHandlerCursesGUI.cpp index 5b70917f11cbd..ff2cd2777af67 100644 --- a/lldb/source/Core/IOHandlerCursesGUI.cpp +++ b/lldb/source/Core/IOHandlerCursesGUI.cpp @@ -327,6 +327,8 @@ class WindowDelegate { virtual const char *WindowDelegateGetHelpText() { return nullptr; } virtual KeyHelp *WindowDelegateGetKeyHelp() { return nullptr; } + + virtual void WindowDelegateProcessEvent(const lldb::EventSP &event_sp) {} }; class HelpDialogDelegate : public WindowDelegate { @@ -799,6 +801,13 @@ class Window : public Surface { subwindow_sp->Draw(force); } + void HandleProcessEvent(const lldb::EventSP &event_sp) { + if (m_delegate_sp) + m_delegate_sp->WindowDelegateProcessEvent(event_sp); + for (auto &subwindow_sp : m_subwindows) + subwindow_sp->HandleProcessEvent(event_sp); + } + bool CreateHelpSubwindow() { if (m_delegate_sp) { const char *text = m_delegate_sp->WindowDelegateGetHelpText(); @@ -4385,6 +4394,11 @@ class Application { ConstString broadcaster_class( broadcaster->GetBroadcasterClass()); if (broadcaster_class == broadcaster_class_process) { + uint32_t event_type = event_sp->GetType(); + if (event_type & (Process::eBroadcastBitSTDOUT | + Process::eBroadcastBitSTDERR)) { + m_window_sp->HandleProcessEvent(event_sp); + } m_update_screen = true; continue; // Don't get any key, just update our view } @@ -6311,6 +6325,233 @@ HandleCharResult HelpDialogDelegate::WindowDelegateHandleChar(Window &window, return eKeyHandled; } +class ConsoleOutputWindowDelegate : public WindowDelegate { +private: + void PollProcessOutput() { + ExecutionContext exe_ctx = + m_debugger.GetCommandInterpreter().GetExecutionContext(); + Process *process = exe_ctx.GetProcessPtr(); + + if (!process || !process->IsAlive()) + return; + + // Buffer for reading output. + char buffer[1024]; + Status error; + + // Read all available stdout. + size_t bytes; + while ((bytes = process->GetSTDOUT(buffer, sizeof(buffer), error)) > 0) + AppendOutput(buffer, bytes, false); + + // Read all available stderr. + while ((bytes = process->GetSTDERR(buffer, sizeof(buffer), error)) > 0) + AppendOutput(buffer, bytes, true); + } + + void AppendOutput(const char *text, size_t len, bool is_stderr) { + if (!text || len == 0) + return; + + std::lock_guard<std::mutex> lock(m_output_mutex); + + // Split text into lines and add to buffer. + std::string remaining = m_partial_line; + remaining.append(text, len); + + size_t start = 0, pos = 0; + while ((pos = remaining.find('\n', start)) != std::string::npos) { + std::string line = remaining.substr(start, pos - start); + if (is_stderr) + line = "[stderr] " + line; + m_output_lines.push_back(line); + + // Keep buffer size under limit. + size_t max_lines = m_debugger.GetGuiMaxConsoleLines(); + while (m_output_lines.size() > max_lines) { + m_output_lines.pop_front(); + if (m_first_visible_line > 0) + --m_first_visible_line; + } + + start = pos + 1; + } + + // Save any remaining partial line. + m_partial_line = remaining.substr(start); + + // Auto-scroll to bottom if enabled. + if (m_auto_scroll && !m_output_lines.empty()) { + m_first_visible_line = + m_output_lines.size() > 0 ? m_output_lines.size() - 1 : 0; + } + } + +public: + ConsoleOutputWindowDelegate(Debugger &debugger) + : m_debugger(debugger), m_first_visible_line(0), m_auto_scroll(true) {} + + ~ConsoleOutputWindowDelegate() override = default; + + void WindowDelegateProcessEvent(const lldb::EventSP &event_sp) override { + if (event_sp->GetType() & + (Process::eBroadcastBitSTDOUT | Process::eBroadcastBitSTDERR)) + PollProcessOutput(); + } + + bool WindowDelegateDraw(Window &window, bool force) override { + std::lock_guard<std::mutex> lock(m_output_mutex); + + window.Erase(); + window.DrawTitleBox(window.GetName()); + + const int width = window.GetWidth(); + const int height = window.GetHeight(); + + // Calculate the visible range. + size_t total_lines = m_output_lines.size(); + if (total_lines == 0) { + window.MoveCursor(2, 1); + window.PutCString("(no output yet)"); + return true; + } + + // Adjust scroll pos if needed. + if (m_first_visible_line >= total_lines) { + m_first_visible_line = total_lines > 0 ? total_lines - 1 : 0; + } + + // Draw visible line. + int visible_height = height - 2; + size_t start_line = m_first_visible_line; + + // If we are at the end, display last N lines. + if (m_auto_scroll || start_line + visible_height > total_lines) { + start_line = total_lines > static_cast<size_t>(visible_height) + ? total_lines - visible_height + : 0; + } + + for (int row = 1; + row <= visible_height && (start_line + row - 1) < total_lines; ++row) { + window.MoveCursor(2, row); + const std::string &line = m_output_lines[start_line + row - 1]; + + // Highlight stderr lines?. + bool is_stderr = (line.find("[stderr]") == 0); + if (is_stderr) + window.AttributeOn(COLOR_PAIR(2)); + + // Truncate line to fit window width. + int available_width = width - 3; + if (static_cast<int>(line.length()) > available_width) + window.PutCString(line.substr(0, available_width).c_str()); + else + window.PutCString(line.c_str()); + + if (is_stderr) + window.AttributeOff(COLOR_PAIR(2)); + } + + return true; + } + + HandleCharResult WindowDelegateHandleChar(Window &window, int key) override { + std::lock_guard<std::mutex> lock(m_output_mutex); + + size_t total_lines = m_output_lines.size(); + int visible_height = window.GetHeight() - 1; + + switch (key) { + case KEY_UP: + if (m_first_visible_line > 0) { + --m_first_visible_line; + m_auto_scroll = false; + } + return eKeyHandled; + + case KEY_DOWN: + if (m_first_visible_line + visible_height < total_lines) + ++m_first_visible_line; + // Re-enable Auto-scroll at bottom. + if (m_first_visible_line + visible_height >= total_lines) + m_auto_scroll = true; + return eKeyHandled; + + case KEY_PPAGE: + if (m_first_visible_line > static_cast<size_t>(visible_height)) + m_first_visible_line -= visible_height; + else + m_first_visible_line = 0; + m_auto_scroll = false; + return eKeyHandled; + + case KEY_NPAGE: + m_first_visible_line += visible_height; + if (m_first_visible_line + visible_height >= total_lines) { + m_first_visible_line = total_lines > static_cast<size_t>(visible_height) + ? total_lines - visible_height + : 0; + m_auto_scroll = true; + } + return eKeyHandled; + + case 'a': + m_auto_scroll = !m_auto_scroll; + if (m_auto_scroll && total_lines > 0) + m_first_visible_line = total_lines > static_cast<size_t>(visible_height) + ? total_lines - visible_height + : 0; + return eKeyHandled; + + case 'c': + m_output_lines.clear(); + m_partial_line.clear(); + m_first_visible_line = 0; + return eKeyHandled; + + case KEY_HOME: + m_first_visible_line = 0; + m_auto_scroll = false; + return eKeyHandled; + + case KEY_END: + m_first_visible_line = total_lines > static_cast<size_t>(visible_height) + ? total_lines - visible_height + : 0; + m_auto_scroll = true; + return eKeyHandled; + + default: + break; + } + + return eKeyNotHandled; + } + + const char *WindowDelegateGetHelpText() override { + return "Console Output view shows stdout and stderr from the process."; + } + + KeyHelp *WindowDelegateGetKeyHelp() override { + static curses::KeyHelp g_source_view_key_help[] = { + {KEY_UP, "Scroll up"}, {KEY_DOWN, "Scroll down"}, + {KEY_PPAGE, "Page up"}, {KEY_NPAGE, "Page down"}, + {KEY_HOME, "Go to top"}, {KEY_END, "Go to bottom"}, + {'h', "Show help dialog"}, {'a', "Toggle auto-scroll"}, + {'c', "Clear output"}, {'\0', nullptr}}; + return g_source_view_key_help; + } + +protected: + Debugger &m_debugger; + std::deque<std::string> m_output_lines; + std::string m_partial_line; + size_t m_first_visible_line = 0; + bool m_auto_scroll = true; + std::mutex m_output_mutex; +}; + class ApplicationDelegate : public WindowDelegate, public MenuDelegate { public: enum { @@ -6342,6 +6583,7 @@ class ApplicationDelegate : public WindowDelegate, public MenuDelegate { eMenuID_ViewSource, eMenuID_ViewVariables, eMenuID_ViewBreakpoints, + eMenuId_ViewConsole, eMenuID_Help, eMenuID_HelpGUIHelp @@ -6352,6 +6594,14 @@ class ApplicationDelegate : public WindowDelegate, public MenuDelegate { ~ApplicationDelegate() override = default; + WindowDelegateSP GetConsoleDelegate() { + if (!m_console_delegate_sp) { + m_console_delegate_sp = + WindowDelegateSP(new ConsoleOutputWindowDelegate(m_debugger)); + } + return m_console_delegate_sp; + } + bool WindowDelegateDraw(Window &window, bool force) override { return false; // Drawing not handled, let standard window drawing happen } @@ -6582,6 +6832,7 @@ class ApplicationDelegate : public WindowDelegate, public MenuDelegate { WindowSP main_window_sp = m_app.GetMainWindow(); WindowSP source_window_sp = main_window_sp->FindSubWindow("Source"); WindowSP variables_window_sp = main_window_sp->FindSubWindow("Variables"); + WindowSP console_window_sp = main_window_sp->FindSubWindow("Console"); WindowSP registers_window_sp = main_window_sp->FindSubWindow("Registers"); const Rect source_bounds = source_window_sp->GetBounds(); @@ -6590,39 +6841,52 @@ class ApplicationDelegate : public WindowDelegate, public MenuDelegate { main_window_sp->RemoveSubWindow(variables_window_sp.get()); - if (registers_window_sp) { + if (console_window_sp) { + Rect console_bounds = console_window_sp->GetBounds(); + console_bounds.origin.x = variables_bounds.origin.x; + console_bounds.size.width = + variables_bounds.size.width + console_bounds.size.width; + console_window_sp->SetBounds(console_bounds); + } else if (registers_window_sp) { // We have a registers window, so give all the area back to the - // registers window + // registers window. Rect registers_bounds = variables_bounds; registers_bounds.size.width = source_bounds.size.width; registers_window_sp->SetBounds(registers_bounds); } else { - // We have no registers window showing so give the bottom area back - // to the source view + // We have no console or registers window showing so give the bottom + // area back to the source view. source_window_sp->Resize(source_bounds.size.width, source_bounds.size.height + variables_bounds.size.height); } } else { - Rect new_variables_rect; - if (registers_window_sp) { + Rect new_vars_rect; + if (console_window_sp) { + // Console exists, so split the area. + const Rect console_bounds = console_window_sp->GetBounds(); + Rect new_console_rect; + console_bounds.VerticalSplitPercentage(0.50, new_vars_rect, + new_console_rect); + } else if (registers_window_sp) { // We have a registers window so split the area of the registers // window into two columns where the left hand side will be the - // variables and the right hand side will be the registers - const Rect variables_bounds = registers_window_sp->GetBounds(); - Rect new_registers_rect; - variables_bounds.VerticalSplitPercentage(0.50, new_variables_rect, - new_registers_rect); - registers_window_sp->SetBounds(new_registers_rect); + // variables and the right hand side will be the registers. + const Rect registers_bounds = registers_window_sp->GetBounds(); + Rect new_regs_rect; + registers_bounds.VerticalSplitPercentage(0.50, new_vars_rect, + new_regs_rect); + registers_window_sp->SetBounds(new_regs_rect); } else { - // No registers window, grab the bottom part of the source window + // No registers or console window, grab the bottom part of the source + // window. Rect new_source_rect; source_bounds.HorizontalSplitPercentage(0.70, new_source_rect, - new_variables_rect); + new_vars_rect); source_window_sp->SetBounds(new_source_rect); } - WindowSP new_window_sp = main_window_sp->CreateSubWindow( - "Variables", new_variables_rect, false); + WindowSP new_window_sp = + main_window_sp->CreateSubWindow("Variables", new_vars_rect, false); new_window_sp->SetDelegate( WindowDelegateSP(new FrameVariablesWindowDelegate(m_debugger))); } @@ -6642,13 +6906,13 @@ class ApplicationDelegate : public WindowDelegate, public MenuDelegate { const Rect variables_bounds = variables_window_sp->GetBounds(); // We have a variables window, so give all the area back to the - // variables window + // variables window. variables_window_sp->Resize(variables_bounds.size.width + registers_window_sp->GetWidth(), variables_bounds.size.height); } else { // We have no variables window showing so give the bottom area back - // to the source view + // to the source view. source_window_sp->Resize(source_bounds.size.width, source_bounds.size.height + registers_window_sp->GetHeight()); @@ -6659,14 +6923,14 @@ class ApplicationDelegate : public WindowDelegate, public MenuDelegate { if (variables_window_sp) { // We have a variables window, split it into two columns where the // left hand side will be the variables and the right hand side will - // be the registers + // be the registers. const Rect variables_bounds = variables_window_sp->GetBounds(); Rect new_vars_rect; variables_bounds.VerticalSplitPercentage(0.50, new_vars_rect, new_regs_rect); variables_window_sp->SetBounds(new_vars_rect); } else { - // No variables window, grab the bottom part of the source window + // No variables window, grab the bottom part of the source window. Rect new_source_rect; source_bounds.HorizontalSplitPercentage(0.70, new_source_rect, new_regs_rect); @@ -6681,6 +6945,66 @@ class ApplicationDelegate : public WindowDelegate, public MenuDelegate { } return MenuActionResult::Handled; + case eMenuId_ViewConsole: { + WindowSP main_window_sp = m_app.GetMainWindow(); + WindowSP source_window_sp = main_window_sp->FindSubWindow("Source"); + WindowSP console_window_sp = main_window_sp->FindSubWindow("Console"); + WindowSP variables_window_sp = main_window_sp->FindSubWindow("Variables"); + WindowSP registers_window_sp = main_window_sp->FindSubWindow("Registers"); + const Rect source_bounds = source_window_sp->GetBounds(); + + if (console_window_sp) { + const Rect console_bounds = console_window_sp->GetBounds(); + main_window_sp->RemoveSubWindow(console_window_sp.get()); + + if (variables_window_sp) { + // Variables window exists, so give Console space to Variables. + Rect variables_bounds = variables_window_sp->GetBounds(); + variables_bounds.size.width = + variables_bounds.size.width + console_bounds.size.width; + variables_window_sp->SetBounds(variables_bounds); + } else if (registers_window_sp) { + // Registers window exists, so give Console space to Registers. + Rect registers_bounds = registers_window_sp->GetBounds(); + registers_bounds.size.width = source_bounds.size.width; + registers_window_sp->SetBounds(registers_bounds); + } else { + // No Variables or Registers window exists. + source_window_sp->Resize(source_bounds.size.width, + source_bounds.size.height + + console_bounds.size.height); + } + } else { + Rect new_console_rect; + if (variables_window_sp) { + // Variable window exists, split area. + const Rect variables_bounds = variables_window_sp->GetBounds(); + Rect new_vars_rect; + variables_bounds.VerticalSplitPercentage(0.50, new_vars_rect, + new_console_rect); + variables_window_sp->SetBounds(new_vars_rect); + } else if (registers_window_sp) { + // Registers window exists, split area. + const Rect registers_bounds = registers_window_sp->GetBounds(); + Rect new_regs_rect; + registers_bounds.VerticalSplitPercentage(0.50, new_console_rect, + new_regs_rect); + registers_window_sp->SetBounds(new_regs_rect); + } else { + // No Registers or Variables window exists, split source area. + Rect new_source_rect; + source_bounds.HorizontalSplitPercentage(0.70, new_source_rect, + new_console_rect); + source_window_sp->SetBounds(new_source_rect); + } + WindowSP new_window_sp = + main_window_sp->CreateSubWindow("Console", new_console_rect, false); + new_window_sp->SetDelegate(GetConsoleDelegate()); + } + touchwin(stdscr); + } + return MenuActionResult::Handled; + case eMenuID_ViewBreakpoints: { WindowSP main_window_sp = m_app.GetMainWindow(); WindowSP threads_window_sp = main_window_sp->FindSubWindow("Threads"); @@ -6728,6 +7052,7 @@ class ApplicationDelegate : public WindowDelegate, public MenuDelegate { protected: Application &m_app; Debugger &m_debugger; + WindowDelegateSP m_console_delegate_sp; }; class StatusBarWindowDelegate : public WindowDelegate { @@ -7635,6 +7960,8 @@ void IOHandlerCursesGUI::Activate() { view_menu_sp->AddSubmenu( std::make_shared<Menu>("Breakpoints", nullptr, 'b', ApplicationDelegate::eMenuID_ViewBreakpoints)); + view_menu_sp->AddSubmenu(std::make_shared<Menu>( + "Console", nullptr, 'o', ApplicationDelegate::eMenuId_ViewConsole)); MenuSP help_menu_sp( new Menu("Help", "F6", KEY_F(6), ApplicationDelegate::eMenuID_Help)); @@ -7658,12 +7985,16 @@ void IOHandlerCursesGUI::Activate() { Rect status_bounds = content_bounds.MakeStatusBar(); Rect source_bounds; Rect variables_bounds; + Rect console_bounds; Rect threads_bounds; Rect source_variables_bounds; + Rect variables_console_bounds; content_bounds.VerticalSplitPercentage(0.80, source_variables_bounds, threads_bounds); source_variables_bounds.HorizontalSplitPercentage(0.70, source_bounds, - variables_bounds); + variables_console_bounds); + variables_console_bounds.VerticalSplitPercentage(0.50, variables_bounds, + console_bounds); WindowSP menubar_window_sp = main_window_sp->CreateSubWindow("Menubar", menubar_bounds, false); @@ -7675,10 +8006,12 @@ void IOHandlerCursesGUI::Activate() { WindowSP source_window_sp( main_window_sp->CreateSubWindow("Source", source_bounds, true)); - WindowSP variables_window_sp( - main_window_sp->CreateSubWindow("Variables", variables_bounds, false)); WindowSP threads_window_sp( main_window_sp->CreateSubWindow("Threads", threads_bounds, false)); + WindowSP variables_window_sp( + main_window_sp->CreateSubWindow("Variables", variables_bounds, false)); + WindowSP console_window_sp( + main_window_sp->CreateSubWindow("Console", console_bounds, false)); WindowSP status_window_sp( main_window_sp->CreateSubWindow("Status", status_bounds, false)); status_window_sp->SetCanBeActive( @@ -7689,6 +8022,7 @@ void IOHandlerCursesGUI::Activate() { WindowDelegateSP(new SourceFileWindowDelegate(m_debugger))); variables_window_sp->SetDelegate( WindowDelegateSP(new FrameVariablesWindowDelegate(m_debugger))); + console_window_sp->SetDelegate(app_delegate_sp->GetConsoleDelegate()); TreeDelegateSP thread_delegate_sp(new ThreadsTreeDelegate(m_debugger)); threads_window_sp->SetDelegate(WindowDelegateSP( new TreeWindowDelegate(m_debugger, thread_delegate_sp))); diff --git a/lldb/test/API/commands/gui/console-output/Makefile b/lldb/test/API/commands/gui/console-output/Makefile new file mode 100644 index 0000000000000..3d0b98f13f3d7 --- /dev/null +++ b/lldb/test/API/commands/gui/console-output/Makefile @@ -0,0 +1,2 @@ +CXX_SOURCES := main.cpp +include Makefile.rules diff --git a/lldb/test/API/commands/gui/console-output/TestGuiConsoleOutput.py b/lldb/test/API/commands/gui/console-output/TestGuiConsoleOutput.py new file mode 100644 index 0000000000000..c21604a752b63 --- /dev/null +++ b/lldb/test/API/commands/gui/console-output/TestGuiConsoleOutput.py @@ -0,0 +1,138 @@ +""" +Test that the 'gui' console output pane displays stdout / stderr from the debugged process +""" + +import lldb +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from lldbsuite.test.lldbpexpect import PExpectTest + + +class TestGuiConsoleOutputTest(PExpectTest): + # PExpect uses many timeouts internally and doesn't play well + # under ASAN on a loaded machine.. + @skipIfAsan + @skipIfCursesSupportMissing + def test_gui_console_output(self): + """Test that console pane prints messages""" + self.build() + + self.launch( + executable=self.getBuildArtifact("a.out"), + dimensions=(100, 500), + run_under=["env", "TERM=xterm"], + ) + + self.expect( + 'br set -o true -f main.cpp -p "// break here begin"', + substrs=["Breakpoint 1", "address ="], + ) + + self.expect( + 'br set -o true -f main.cpp -p "// break here end"', + substrs=["Breakpoint 2", "address ="], + ) + + self.expect("run", substrs=["stop reason ="]) + + escape_key = chr(27).encode() + + # Start the GUI. + self.child.sendline("gui") + + # Check for gui elements in Menu bar (top of screen) + # We expect these in the order they appear to avoid consumption issues + self.child.expect_exact("Target") + self.child.expect_exact("Process") + self.child.expect_exact("View") + + # Check for window titles (middle of screen) + self.child.expect_exact("Sources") + self.child.expect_exact("Console") + + # The Console window show this message before continuing + self.child.expect_exact("(no output yet)") + + # Continue program execution + self.child.send("c") + + # Check console output for messages + self.child.expect_exact("Hello from stdout line 1") + self.child.expect_exact("Hello from stderr line 3") + + # Check for large output (verify buffer draining) + self.child.expect_exact("Large output line 0") + self.child.expect_exact("Large output line 99") + + # Wait for Breakpoint 2 + self.child.expect_exact("stop reason") + + # Press escape to quit the gui + self.child.send(escape_key) + + self.expect_prompt() + self.quit() + + @skipIfAsan + @skipIfCursesSupportMissing + def test_gui_console_navigate(self): + """Test that console pane navigation works""" + self.build() + + self.launch( + executable=self.getBuildArtifact("a.out"), + dimensions=(100, 500), + run_under=["env", "TERM=xterm"], + ) + + self.expect( + 'br set -o true -f main.cpp -p "// break here begin"', + substrs=["Breakpoint 1", "address ="], + ) + + self.expect( + 'br set -o true -f main.cpp -p "// break here end"', + substrs=["Breakpoint 2", "address ="], + ) + + self.expect("run", substrs=["stop reason ="]) + + escape_key = chr(27).encode() + tab_key = chr(9).encode() + + # Start the GUI. + self.child.sendline("gui") + + # Match elements in top-to-bottom order + self.child.expect_exact("Target") + self.child.expect_exact("Sources") + self.child.expect_exact("Console") + + # The Console window show this message before continuing + self.child.expect_exact("(no output yet)") + + # Continue program execution + self.child.send("c") + + # Check console output for messages + self.child.expect_exact("Hello from stdout line 1") + + # Wait for Breakpoint 2 + self.child.expect_exact("stop reason") + + # Tab to console + self.child.send(tab_key) # Sources -> Threads + self.child.send(tab_key) # Threads -> Variables + self.child.send(tab_key) # Variables -> Console + + # Clear Console output + self.child.send("c") + + # The Console window show this message after clear + self.child.expect_exact("(no output yet)") + + # Press escape to quit the gui + self.child.send(escape_key) + + self.expect_prompt() + self.quit() diff --git a/lldb/test/API/commands/gui/console-output/main.cpp b/lldb/test/API/commands/gui/console-output/main.cpp new file mode 100644 index 0000000000000..9800cd50e04bd --- /dev/null +++ b/lldb/test/API/commands/gui/console-output/main.cpp @@ -0,0 +1,34 @@ +#include <chrono> +#include <iostream> +#include <thread> + +void generate_output() { + for (unsigned i = 1; i < 4; ++i) { + std::cout << "Hello from stdout line " << i << std::endl; + std::cerr << "Hello from stderr line " << i << std::endl; + } +} + +void generate_large_output() { + for (unsigned i = 0; i < 100; ++i) { + std::cout << "Large output line " << i + << " to test buffer draining logic in the GUI console." + << std::endl; + } +} + +int main(int argc, char *argv[]) { + int test_var = 42; + + // Break before output. + int break_here = 0; // break here begin + + // Generate stdout/stderr output. + generate_output(); + generate_large_output(); + + // Wait to capture output. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + return 0; // break here end +} diff --git a/llvm/docs/ReleaseNotes.md b/llvm/docs/ReleaseNotes.md index 9c8069b29d1e9..424e67b8b4235 100644 --- a/llvm/docs/ReleaseNotes.md +++ b/llvm/docs/ReleaseNotes.md @@ -255,6 +255,7 @@ Makes programs 10x faster by doing Special New Thing. example, `breakpoint disable .` disables the just-hit breakpoint location. Another usage is to automate a command to run at the current location: `breakpoint command add -o 'p my_var' .`. * The `apropos` command now highlights matching keywords in its output when color is enabled. +* The TUI mode (enabled with the `gui` command) now has a real-time console output pane. stdout / stderr messages get redirected to this pane when it is enabled. #### Deprecated APIs _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
