Repository: pwndbg/pwndbg

Issue:
Enhance thread context
The thread context can be improved so that:
1. It shows the number of all threads in the top bar e..g `[ THREADS (20) ]`
2. It limits the number of displayed threads via some config variable
* When there are more threads then the limit, it should display current thread and other threads up to the limit and the following text: "... (use `ctx thread` or `set context-threads-limit <N>`)"



Discussion:
Impl details:
* https://github.com/pwndbg/pwndbg/blob/dev/pwndbg/commands/context.py#L853-L897
* https://github.com/pwndbg/pwndbg/blob/dev/pwndbg/commands/context.py#L620C5-L627
Any objections to using the tabulate library to display this information?
Nope, it probably makes sense to use it


Resolving patch:
diff --git a/pwndbg/commands/context.py b/pwndbg/commands/context.py
--- a/pwndbg/commands/context.py
+++ b/pwndbg/commands/context.py
@@ -81,6 +81,11 @@ def clear_screen(out=sys.stdout) -> None:
     "regs disasm code ghidra stack backtrace expressions threads",
     "which context sections are displayed (controls order)",
 )
+config_max_threads_display = pwndbg.gdblib.config.add_param(
+    "context-max-threads",
+    4,
+    "maximum number of threads displayed by the context command",
+)
 
 # Storing output configuration per section
 outputs: dict[str, str] = {}
@@ -851,48 +856,57 @@ def get_thread_status(thread):
 
 
 def context_threads(with_banner=True, target=sys.stdout, width=None):
-    threads = gdb.selected_inferior().threads()[::-1]
+    all_threads = gdb.selected_inferior().threads()[::-1]
+    displayed_threads = all_threads[: int(config_max_threads_display)]
+    num_threads_not_shown = len(all_threads) - len(displayed_threads)
 
-    if len(threads) < 2:
+    if len(displayed_threads) < 2:
         return []
 
-    selected_thread = gdb.selected_thread()
-    selected_frame = gdb.selected_frame()
+    original_thread = gdb.selected_thread()
 
-    out = []
+    out = [pwndbg.ui.banner(f"threads ({len(all_threads)} total)", target=target, width=width)]
     max_name_length = 0
 
-    for thread in threads:
+    for thread in displayed_threads:
         name = thread.name or ""
         if len(name) > max_name_length:
             max_name_length = len(name)
 
-    for thread in threads:
-        thread.switch()
-        frame = gdb.selected_frame()
-
-        selected = " ►" if thread is selected_thread else "  "
-
-        symbol = pwndbg.gdblib.symbol.get(frame.pc())
-        status = get_thread_status(thread)
-
+    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)
+        status = get_thread_status(thread)
 
         line = (
             f" {selected} {thread.global_num}\t"
             f'"{pwndbg.color.cyan(name)}" '
             f'{" " * padding}'
-            f"{status}: {M.get(frame.pc())}"
+            f"{status}: "
         )
-        if symbol:
-            line += f" <{pwndbg.color.bold(pwndbg.color.green(symbol))}> "
+
+        if thread.is_stopped():
+            thread.switch()
+            pc = gdb.selected_frame().pc()
+
+            pc_colored = M.get(pc)
+            symbol = pwndbg.gdblib.symbol.get(pc)
+
+            line += f"{pc_colored}"
+            if symbol:
+                line += f" <{pwndbg.color.bold(pwndbg.color.green(symbol))}> "
+
         out.append(line)
 
-    out.insert(0, pwndbg.ui.banner("threads", target=target, width=width))
+    if num_threads_not_shown:
+        out.append(
+            pwndbg.lib.tips.color_tip(
+                f"Not showing {num_threads_not_shown} threads. Use `set context-max-threads <number of threads>` to change this."
+            )
+        )
 
-    selected_thread.switch()
-    selected_frame.select()
+    original_thread.switch()
 
     return out
 


Test patch: