diff --git a/FEATURES.md b/FEATURES.md index 989ae3881..6a6b50dcc 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -71,14 +71,21 @@ import splitmind end ``` -The context sections are available as native gdb TUI windows as well as `pwndbg_[sectionname]` windows. +#### GDB TUI +The context sections are available as native [GDB TUI](https://sourceware.org/gdb/current/onlinedocs/gdb.html/TUI.html) windows named `pwndbg_[sectionname]`. -Try creating a layout and selecting it: +There are some predefined layouts coming with pwndbg which you can select using `layout pwndbg` or `layout pwndbg_code`. + +To create [your own layout](https://sourceware.org/gdb/current/onlinedocs/gdb.html/TUI-Commands.html) and selecting it use normal `tui new-layout` syntax like: ``` -tui new-layout pwndbg {-horizontal { { -horizontal { pwndbg_code 2 pwndbg_disasm 8 } 2 { { -horizontal pwndbg_legend 8 pwndbg_control 2 } 1 pwndbg_regs 6 pwndbg_stack 6 } 3 } 7 cmd 3 } 3 { pwndbg_backtrace 1 } 1 } 1 status 1 -layout pwndbg +tui new-layout pwndbg_custom {-horizontal { { -horizontal { pwndbg_code 1 pwndbg_disasm 1 } 2 { {-horizontal pwndbg_legend 8 pwndbg_control 2 } 1 pwndbg_regs 6 pwndbg_stack 6 } 3 } 7 cmd 3 } 3 { pwndbg_backtrace 2 pwndbg_threads 1 pwndbg_expressions 2 } 1 } 1 status 1 +layout pwndbg_custom ``` +![](caps/context_tui.png) + +Use `focus cmd` to focus the command window and have the arrow keys scroll through the command history again. `tui disable` to disable TUI mode and go back to CLI mode when running commands with longer output. `ctrl-x + a` toggles between TUI and CLI mode quickly. Hold shift to ignore the TUI mouse integration and use the mouse normally to select text or copy data. + ### Watch Expressions You can add expressions to be watched by the context. diff --git a/caps/context_tui.png b/caps/context_tui.png new file mode 100644 index 000000000..b91ff0ae9 Binary files /dev/null and b/caps/context_tui.png differ diff --git a/pwndbg/commands/context.py b/pwndbg/commands/context.py index 51b956915..a1b65f86f 100644 --- a/pwndbg/commands/context.py +++ b/pwndbg/commands/context.py @@ -1139,24 +1139,33 @@ def context_threads(with_banner=True, target=sys.stdout, width=None): if len(displayed_threads) < 2: return [] - out = [pwndbg.ui.banner(f"threads ({len(all_threads)} total)", target=target, width=width)] + out = ( + [pwndbg.ui.banner(f"threads ({len(all_threads)} total)", target=target, width=width)] + if with_banner + else [] + ) max_name_length = 0 + max_global_num_len = 0 for thread in displayed_threads: name = thread.name or "" if len(name) > max_name_length: max_name_length = len(name) + if len(str(thread.global_num)) > max_global_num_len: + max_global_num_len = len(str(thread.global_num)) for thread in filter(lambda t: t.is_valid(), displayed_threads): selected = " ►" if thread is original_thread else " " name = thread.name if thread.name is not None else "" - padding = max_name_length - len(name) + name_padding = max_name_length - len(name) + global_num_padding = max(2, max_global_num_len - len(str(thread.global_num))) status = get_thread_status(thread) line = ( - f" {selected} {thread.global_num}\t" + f" {selected} {thread.global_num} " + f"{' ' * global_num_padding}" f'"{pwndbg.color.cyan(name)}" ' - f'{" " * padding}' + f'{" " * name_padding}' f"{status}: " ) diff --git a/pwndbg/dbg/gdb.py b/pwndbg/dbg/gdb.py index 78cdf70d7..7de488ae2 100644 --- a/pwndbg/dbg/gdb.py +++ b/pwndbg/dbg/gdb.py @@ -997,6 +997,8 @@ class GDB(pwndbg.dbg_mod.Debugger): except gdb.error: pass + pwndbg.gdblib.tui.setup() + # Reading Comment file from pwndbg.commands import comments diff --git a/pwndbg/gdblib/tui/__init__.py b/pwndbg/gdblib/tui/__init__.py index 3564d3ad5..c23180f6b 100644 --- a/pwndbg/gdblib/tui/__init__.py +++ b/pwndbg/gdblib/tui/__init__.py @@ -1,4 +1,42 @@ from __future__ import annotations +import gdb + import pwndbg.gdblib.tui.context import pwndbg.gdblib.tui.control + + +def setup() -> None: + tui_layouts = [ + ( + "tui new-layout pwndbg " + "{-horizontal " + " { " + " { -horizontal " + " { pwndbg_disasm 1 } 2 " + " { " + " { -horizontal pwndbg_legend 8 pwndbg_control 2 } 1 pwndbg_regs 6 pwndbg_stack 6 " + " } 3 " + " } 7 cmd 3 " + " } 3 { pwndbg_backtrace 2 pwndbg_threads 1 pwndbg_expressions 2 } 1 " + "} 1 status 1" + ), + ( + "tui new-layout pwndbg_code " + "{-horizontal " + " { " + " { -horizontal " + " { pwndbg_code 1 pwndbg_disasm 1 } 2 " + " { " + " { -horizontal pwndbg_legend 8 pwndbg_control 2 } 1 pwndbg_regs 6 pwndbg_stack 6 " + " } 3 " + " } 7 cmd 3 " + " } 3 { pwndbg_backtrace 2 pwndbg_threads 1 pwndbg_expressions 2 } 1 " + "} 1 status 1" + ), + ] + for layout in tui_layouts: + try: + gdb.execute(layout) + except gdb.error: + pass diff --git a/pwndbg/gdblib/tui/context.py b/pwndbg/gdblib/tui/context.py index 4aaa8c4be..905456b58 100644 --- a/pwndbg/gdblib/tui/context.py +++ b/pwndbg/gdblib/tui/context.py @@ -7,6 +7,8 @@ from typing import Pattern import gdb +import pwndbg +from pwndbg.color import message from pwndbg.commands.context import context from pwndbg.commands.context import context_sections from pwndbg.commands.context import contextoutput @@ -26,7 +28,8 @@ class ContextTUIWindow: _ansi_escape_regex: Pattern[str] _enabled: bool - _static_enabled = False + _static_enabled: bool = True + _context_windows: List[ContextTUIWindow] = [] def __init__(self, tui_window: "gdb.TuiWindow", section: str) -> None: self._tui_window = tui_window @@ -43,8 +46,10 @@ class ContextTUIWindow: self._enabled = False self._enable() gdb.events.before_prompt.connect(self._before_prompt_listener) + ContextTUIWindow._context_windows.append(self) def close(self) -> None: + ContextTUIWindow._context_windows.remove(self) if self._enabled: self._disable() gdb.events.before_prompt.disconnect(self._before_prompt_listener) @@ -53,6 +58,19 @@ class ContextTUIWindow: # render is called again after the TUI was disabled self._verify_enabled_state() + if ( + not self._lines + and self._section != "legend" + and self._section not in str(pwndbg.config.context_sections) + ): + self._tui_window.write( + message.warn( + f"Section '{self._section}' is not in 'context-sections' and won't be updated automatically." + ), + True, + ) + return + height = self._tui_window.height width = self._tui_window.width start = self._vscroll_start @@ -93,12 +111,10 @@ class ContextTUIWindow: self.render() def _enable(self): - _static_enabled = True self._update() self._enabled = True def _disable(self): - _static_enabled = False self._old_width = 0 resetcontextoutput(self._section) self._enabled = False @@ -128,13 +144,15 @@ class ContextTUIWindow: is_valid = self._tui_window.is_valid() if is_valid: if not self._enabled: - should_trigger_context = not self._static_enabled - self._enable() - if should_trigger_context and gdb.selected_inferior().pid: + for context_window in ContextTUIWindow._context_windows: + context_window._enable() + if not ContextTUIWindow._static_enabled and pwndbg.dbg.selected_inferior().alive(): context() + ContextTUIWindow._static_enabled = True else: if self._enabled: self._disable() + ContextTUIWindow._static_enabled = False return is_valid def _ansi_substr(self, line: str, start_char: int, end_char: int) -> str: @@ -145,12 +163,12 @@ class ContextTUIWindow: colored_idx = 0 char_count = 0 while colored_idx < len(line): - c = line[colored_end_idx] + c = line[colored_idx] # collect all ansi escape sequences before the start of the colored substring # as well as after the end of the colored substring # skip them while counting the characters to slice if c == "\x1b": - m = self._ansi_escape_regex.match(line[colored_end_idx:]) + m = self._ansi_escape_regex.match(line[colored_idx:]) if m: colored_idx += m.end() if char_count < start_char: @@ -175,15 +193,16 @@ class ContextTUIWindow: ) -sections = ["legend"] + [ - section.__name__.replace("context_", "") for section in context_sections.values() -] -for section_name in sections: - # https://github.com/python/mypy/issues/12557 - target_func: Callable[..., gdb._Window] = ( - lambda window, section_name=section_name: ContextTUIWindow(window, section_name) - ) - gdb.register_window_type( - "pwndbg_" + section_name, - target_func, - ) +if hasattr(gdb, "register_window_type"): + sections = ["legend"] + [ + section.__name__.replace("context_", "") for section in context_sections.values() + ] + for section_name in sections: + # https://github.com/python/mypy/issues/12557 + target_func: Callable[..., gdb._Window] = ( + lambda window, section_name=section_name: ContextTUIWindow(window, section_name) + ) + gdb.register_window_type( + "pwndbg_" + section_name, + target_func, + ) diff --git a/pwndbg/lib/tips.py b/pwndbg/lib/tips.py index c555dafe3..d85954a55 100644 --- a/pwndbg/lib/tips.py +++ b/pwndbg/lib/tips.py @@ -47,6 +47,7 @@ TIPS: List[str] = [ "Need to `mmap` or `mprotect` memory in the debugee? Use commands with the same name to inject and run such syscalls", "Use `hi` to see if a an address belongs to a glibc heap chunk", "Use `contextprev` and `contextnext` to display a previous context output again without scrolling", + "Try splitting the context output into multiple TUI windows using `layout pwndbg` (`tui disable` or `ctrl-x + a` to go back to CLI mode)", ]