[Lldb-commits] [lldb] cd25475 - Fix for #177160 : [lldb] Real-time console pane for output in lldb tui (#197473)
via lldb-commits
lldb-commits at lists.llvm.org
Thu May 14 08:30:28 PDT 2026
Author: Nagesh Nazare
Date: 2026-05-14T16:30:22+01:00
New Revision: cd25475854ecf2b564f91db7f6267edca7868d41
URL: https://github.com/llvm/llvm-project/commit/cd25475854ecf2b564f91db7f6267edca7868d41
DIFF: https://github.com/llvm/llvm-project/commit/cd25475854ecf2b564f91db7f6267edca7868d41.diff
LOG: Fix for #177160 : [lldb] Real-time console pane for output in lldb tui (#197473)
This Change:
The stalling testcase in the PR: #177160 has been fixed.
- The cause of it was that the test required to get line 0 & line 99
from the console output with auto-scroll on. With limited display area,
this test was unreliable and impossible to achieve - which caused it to
wait / hang there.
Original Change :
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
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..c6738789a1205
--- /dev/null
+++ b/lldb/test/API/commands/gui/console-output/TestGuiConsoleOutput.py
@@ -0,0 +1,162 @@
+"""
+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 ="],
+ )
+
+ # Intermediate breakpoint after stdout/stderr output but before large
+ # output. This ensures those lines are visible in the console window
+ # (only 6 lines exist) when the process pauses.
+ self.expect(
+ 'br set -o true -f main.cpp -p "// break here middle"',
+ substrs=["Breakpoint 2", "address ="],
+ )
+
+ self.expect(
+ 'br set -o true -f main.cpp -p "// break here end"',
+ substrs=["Breakpoint 3", "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)
+ 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 shows this message before any output.
+ self.child.expect_exact("(no output yet)")
+
+ # Continue to the intermediate breakpoint (after generate_output()).
+ self.child.send("c")
+
+ # The GUI draws the Threads pane (which shows "stop reason") BEFORE
+ # the Console pane in the byte stream. Check stop reason first so it
+ # is not skipped over when scanning for the console text below.
+ self.child.expect_exact("stop reason")
+
+ # The console pane is drawn after the threads pane in the same redraw
+ # cycle, so these lines appear later in the byte stream.
+ # Only 6 lines of output exist at this stop, so all are visible.
+ self.child.expect_exact("Hello from stdout line 1")
+ self.child.expect_exact("Hello from stderr line 3")
+
+ # Continue past the large output to the final breakpoint.
+ self.child.send("c")
+
+ # Again check stop reason before console output for the same reason.
+ self.child.expect_exact("stop reason")
+
+ # Large output lines near the end are guaranteed visible in the
+ # auto-scrolled console window.
+ self.child.expect_exact("Large output line 99")
+
+ # 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 ="],
+ )
+
+ # Intermediate breakpoint after stdout/stderr, before large output.
+ self.expect(
+ 'br set -o true -f main.cpp -p "// break here middle"',
+ substrs=["Breakpoint 2", "address ="],
+ )
+
+ self.expect(
+ 'br set -o true -f main.cpp -p "// break here end"',
+ substrs=["Breakpoint 3", "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 shows this message before any output.
+ self.child.expect_exact("(no output yet)")
+
+ # Continue to the intermediate breakpoint (after generate_output()).
+ self.child.send("c")
+
+ # Threads pane ("stop reason") is drawn before the Console pane, so
+ # check it first to avoid consuming it while scanning for console text.
+ self.child.expect_exact("stop reason")
+
+ # Check that stdout output is visible (only 6 lines at this stop).
+ self.child.expect_exact("Hello from stdout line 1")
+
+ # Tab to console window (Sources -> Threads -> Variables -> Console)
+ self.child.send(tab_key)
+ self.child.send(tab_key)
+ self.child.send(tab_key)
+
+ # Clear Console output ('c' in the Console window clears output)
+ self.child.send("c")
+
+ # The Console window shows 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..ae677f6c45aa5
--- /dev/null
+++ b/lldb/test/API/commands/gui/console-output/main.cpp
@@ -0,0 +1,39 @@
+#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();
+
+ // Break after stdout/stderr to verify it appears in console before scrolling.
+ int break_after_output = 0; // break here middle
+
+ // Generate large output to test buffer draining.
+ 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 fffd696e59baf..2e3f6146789b4 100644
--- a/llvm/docs/ReleaseNotes.md
+++ b/llvm/docs/ReleaseNotes.md
@@ -274,6 +274,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
More information about the lldb-commits
mailing list