Skip to content

Commit 7fe9133

Browse files
author
Magne Hov
committed
improve TUI windows
- support scrolling TUI windows - add layouts for UDB commands - breakpoints - locals - timeline - threads
1 parent 31cde9a commit 7fe9133

File tree

3 files changed

+318
-83
lines changed

3 files changed

+318
-83
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Clone the repository or copy the following files into your current working direc
1313
- `libpython.py`
1414
- `libpython_extensions.py`
1515
- `libpython_ui.py`
16+
- `tui_windows.py`
1617

1718
and start UDB while sourcing the `libpython.gdb` file from the same directory
1819

libpython_ui.py

+64-83
Original file line numberDiff line numberDiff line change
@@ -1,129 +1,110 @@
1-
import subprocess
1+
import functools
2+
import os
23

34
import gdb
5+
import pygments
6+
import pygments.lexers
7+
import pygments.formatters
48

9+
import libpython
10+
import libpython_extensions
11+
import tui_windows
512

6-
def highlight_python(text):
13+
14+
def highlight_python(content: bytes) -> bytes:
715
"""
8-
Pipe bytes through the highlight program to add Python syntax highlighting.
16+
Applies Python syntax highlighting and prepends line numbers to provided content.
917
"""
10-
if getattr(highlight_python, "failed", False):
11-
return text
12-
result = subprocess.run(
13-
["highlight", "--syntax=python", "--out-format=ansi"],
14-
stdout=subprocess.PIPE,
15-
input=text,
16-
check=False,
18+
return pygments.highlight(
19+
content, pygments.lexers.PythonLexer(), pygments.formatters.TerminalFormatter(linenos=True)
1720
)
18-
if result.returncode:
19-
print("Failed to provide syntax highlighting for Python.")
20-
print("Please install the `highlight` program.")
21-
highlight_python.failed = True
22-
return text
23-
return result.stdout
2421

2522

26-
def register_window(name):
23+
@functools.cache
24+
def get_highlighted_file_content(filename: str) -> str:
2725
"""
28-
Register a TUI window and define a new layout for it.
26+
Returns the content of the Python source file with syntax highlighting.
2927
"""
28+
with libpython_extensions.PythonSubstitutePath.open(os.fsencode(filename), "r") as f:
29+
content = f.read()
30+
return highlight_python(content)
3031

31-
def decorator(cls):
32-
gdb.register_window_type(name, cls)
33-
gdb.execute(f"tui new-layout {name} {name} 1 status 1 cmd 1")
34-
return cls
35-
36-
return decorator
37-
38-
39-
class Window:
40-
title: str | None = None
41-
42-
def __init__(self, tui_window):
43-
self._tui_window = tui_window
44-
self._tui_window.title = self.title
45-
gdb.events.before_prompt.connect(self.render)
4632

47-
def get_lines(self):
48-
raise NotImplementedError()
33+
def get_filename_and_line() -> tuple[str, int]:
34+
"""
35+
Returns the path to the current Python source file and the current line number.
36+
"""
37+
# py-list requires an actual PyEval_EvalFrameEx frame:
38+
frame = libpython.Frame.get_selected_bytecode_frame()
39+
if not frame:
40+
raise gdb.error("Unable to locate gdb frame for python bytecode interpreter")
4941

50-
def render(self):
51-
if not self._tui_window.is_valid():
52-
return
42+
pyop = frame.get_pyop()
43+
if not pyop or pyop.is_optimized_out():
44+
raise gdb.error(libpython.UNABLE_READ_INFO_PYTHON_FRAME)
5345

54-
# Truncate output
55-
lines = self.get_lines()[:self._tui_window.height]
56-
lines = (line[:self._tui_window.width - 1] for line in lines)
46+
filename = pyop.filename()
47+
lineno = pyop.current_line_num()
48+
if lineno is None:
49+
raise gdb.error("Unable to read python frame line number")
50+
return filename, lineno
5751

58-
output = "\n".join(lines)
59-
self._tui_window.write(output, True)
6052

61-
def close(self):
62-
gdb.events.before_prompt.disconnect(self.render)
53+
@tui_windows.register_window("python-source")
54+
class PythonSourceWindow(tui_windows.ScrollableWindow):
55+
title = "Python Source"
6356

57+
def get_content(self):
58+
filename, line = get_filename_and_line()
59+
lines = get_highlighted_file_content(filename).splitlines()
60+
prefixed_lines = [(" > " if i == line else " ") + l for i, l in enumerate(lines, start=1)]
6461

65-
@register_window("python-source")
66-
class PythonSourceWindow(Window):
67-
title = "Python Source"
62+
# Set vertical scroll offset to center the current line
63+
half_window_height = self._tui_window.height // 2
64+
self.vscroll_offset = line - half_window_height
6865

69-
def get_lines(self):
70-
python_source = gdb.execute("py-list", to_string=True).encode("utf-8")
71-
return highlight_python(python_source).decode("utf-8").splitlines()
66+
return "\n".join(prefixed_lines)
7267

7368

74-
@register_window("python-backtrace")
75-
class PythonBacktraceWindow(Window):
69+
@tui_windows.register_window("python-backtrace")
70+
class PythonBacktraceWindow(tui_windows.ScrollableWindow):
7671
title = "Python Backtrace"
7772

78-
def get_lines(self):
79-
return gdb.execute("py-bt", to_string=True).splitlines()
73+
def get_content(self):
74+
return gdb.execute("py-bt", to_string=True)
8075

8176

82-
@register_window("python-locals")
83-
class PythonLocalsWindow(Window):
77+
@tui_windows.register_window("python-locals")
78+
class PythonLocalsWindow(tui_windows.ScrollableWindow):
8479
title = "Local Python Variables"
8580

86-
def get_lines(self):
87-
return gdb.execute("py-locals", to_string=True).splitlines()
81+
def get_content(self):
82+
return gdb.execute("py-locals", to_string=True)
8883

8984

90-
@register_window("python-bytecode")
91-
class PythonBytecodeWindow(Window):
85+
@tui_windows.register_window("python-bytecode")
86+
class PythonBytecodeWindow(tui_windows.ScrollableWindow):
9287
title = "Python Bytecode"
9388

94-
def get_lines(self):
89+
def get_lines(self) -> list[str]:
9590
lines = gdb.execute("py-dis", to_string=True).splitlines()
96-
total_lines = len(lines)
97-
height = self._tui_window.height
98-
if total_lines < height:
99-
return lines
10091

101-
current_line = None
102-
for index, line in enumerate(lines, 1):
92+
# Set vertical scroll offset to center the current line
93+
for index, line in enumerate(lines, start=1):
10394
if "-->" in line:
104-
current_line = index
105-
break
106-
else:
107-
return lines[:height]
95+
half_window_height = self._tui_window.height // 2
96+
self.vscroll_offset = index - half_window_height
10897

109-
first_half = height // 2
110-
second_half = height - first_half
111-
if current_line < first_half:
112-
return lines[:height]
113-
if current_line + second_half > total_lines:
114-
return lines[-height:]
115-
return lines[current_line - first_half : current_line + second_half]
98+
return lines
11699

117100

118101
# Define a layout with all Python windows
119102
gdb.execute(
120103
" ".join(
121104
(
122105
"tui new-layout python",
123-
"python-backtrace 2",
124-
"{-horizontal python-bytecode 1 python-locals 1} 2",
125-
"python-source 2",
126-
"status 1 cmd 1",
106+
"{-horizontal {python-source 2 status 1 cmd 1} 3",
107+
"{python-locals 1 python-backtrace 1 python-bytecode 1 timeline 1} 2} 1",
127108
)
128109
)
129110
)

0 commit comments

Comments
 (0)