Skip to content

etorth/tgdb

Repository files navigation

tgdb — Vi-like TUI front-end for GDB

tgdb is a terminal-based front-end for GDB, reimplemented in Python using Textual for the TUI and Pygments for syntax highlighting. It aims for 100% compatibility with cgdb in terms of keybindings, configuration, and behaviour.


Features

  • Split-screen TUI: syntax-highlighted source window above, GDB console below
  • Vi-like keybindings in the source window
  • Syntax highlighting for C, C++, Python, Rust, Go, Ada, Fortran, and more (via Pygments)
  • Visual breakpoint management: set/delete/enable/disable with Space
  • Executing line indicator: shortarrow, longarrow, highlight, or block styles
  • Regex search in source window (/, ?, n, N) with optional hlsearch
  • Marks (m[a-z/A-Z] / '[a-z/A-Z]) — local and global
  • File dialog (o) to browse and select source files
  • GDB scrollback buffer with configurable size and vi-style scroll mode
  • ANSI colour support in GDB output window
  • ~/.config/tgdb/tgdbrc config file (XDG_CONFIG_HOME/tgdb/tgdbrc)
  • Numeric prefix for movement: [N]j, [N]k, [N]G (e.g. 20j = down 20 lines)
  • Horizontal vim navigation: 0 = beginning of line, ^ = first char, $ = end of line
  • Goto line: :N jumps to line N in the source window (:+N / :-N for relative)
  • Command history: persistent across sessions, !! reruns last command, !N reruns entry N
  • Comments in command mode: lines starting with # are no-ops (useful in rc files and history)
  • Async Python scripting: :python await foo(), heredoc :python << EOF, top-level await

Installation

pip install -e .
# or
pip install textual pygments ptyprocess pyte
python -m tgdb

Usage

tgdb [OPTIONS] [PROGRAM [CORE|PID]]

Options:
  -d, --debugger DEBUGGER   Path to GDB executable (default: gdb)
  -r, --rcfile FILE         Configuration file (default: ~/.config/tgdb/tgdbrc)
                            Use --rcfile NONE (uppercase) to skip loading
  --args                    Pass remaining args as program + arguments to GDB
  --cd DIR                  Change to DIR before starting GDB

Examples:

tgdb myprogram
tgdb -d /usr/bin/gdb myprogram
tgdb --args myprogram arg1 arg2
tgdb myprogram core

Keybindings

CGDB mode (source window, default mode)

Key Action
ESC Stay in / return to CGDB mode
i Switch to GDB mode
s Switch to GDB scroll mode
[N]j / [N]↓ Move cursor down N lines (default 1)
[N]k / [N]↑ Move cursor up N lines (default 1)
h / Move left (horizontal scroll)
l / Move right (horizontal scroll)
0 Go to beginning of line (reset horizontal scroll)
^ Go to first visible char (same as 0)
$ Go to end of current line
Ctrl-f / PageDn Page down
Ctrl-b / PageUp Page up
Ctrl-d Half page down
Ctrl-u Half page up
gg Go to top
[N]G Go to line N (or bottom if no prefix)
H / M / L Screen top / middle / bottom
/ Forward search
? Backward search
n / N Next / previous search match
Space Toggle breakpoint at current line
t Set temporary breakpoint
o Open file dialog
m[a-z/A-Z] Set mark
'[a-z/A-Z] Jump to mark
'' Jump to last jump location
'. Jump to executing line
Ctrl-W Toggle split orientation
- / = Shrink / grow source window by 1
_ / + Shrink / grow source window by 25%
Ctrl-l Redraw screen
F5 run
F6 continue
F7 finish
F8 next
F10 step
: Enter command mode

GDB mode (GDB console)

Key Action
ESC Switch to CGDB mode
PageUp Enter scroll mode
/ Command history
Ctrl-C Interrupt GDB
Enter Send command to GDB

Scroll mode (GDB console scrollback)

Key Action
ESC / q / i / Enter Exit scroll mode
j / Scroll down
k / Scroll up
PageUp / PageDn Page up / down
Ctrl-u / Ctrl-d Half page
gg / G Beginning / end of buffer
/ / ? / n / N Search

File dialog

Key Action
q / ESC Close dialog
j / k Move down / up
Enter Open selected file
/ / ? / n / N Search files

Configuration

tgdb reads ~/.config/tgdb/tgdbrc (or $XDG_CONFIG_HOME/tgdb/tgdbrc) on startup. Commands can also be typed interactively via :.

Use --rcfile NONE (uppercase) to skip loading the rc file.

Comments in command mode

Any line beginning with optional whitespace then # is a comment and is ignored:

# This line is a comment
set hlsearch     # NOT a comment — '#' only counts at line start

Comments are valid in rc files, in :source-d files, and in interactive command mode. They are also recorded in history (the session-delimiter line # tgdb begins ... uses this mechanism).

:set options

Option Abbrev Default Description
autosourcereload asr on Auto-reload changed source files
cgdbmodekey=KEY ESC Key to enter CGDB mode
color on Enable colour support
debugwincolor dwc on Show ANSI colours in GDB window
disasm dis off Show disassembly
executinglinedisplay=STYLE eld longarrow shortarrow|longarrow|highlight|block
history=N 1024 Command history buffer size (0 = disabled)
hlsearch hls off Highlight all search matches
ignorecase ic off Case-insensitive search
scrollbackbuffersize=N sbbs 10000 GDB scrollback lines
selectedlinedisplay=STYLE sld block Same styles as above
showmarks on Show marks in gutter
showdebugcommands sdc off Show GDB commands sent
tabstop=N ts 8 Tab width
timeout to on Timeout on key maps
timeoutlen=N tm 1000 Map timeout (ms)
ttimeout on Timeout on key codes
ttimeoutlen=N ttm 100 Key code timeout (ms)
winminheight=N wmh 0 Minimum window height
winminwidth=N wmw 0 Minimum window width
winsplit=STYLE even src_full|src_big|even|gdb_big|gdb_full
winsplitorientation=STYLE wso vertical horizontal|vertical
wrapscan ws on Search wraps around end of file

Setting history=0 disables history (buffer cleared immediately). Use a very large number (e.g. set history=9999999) for effectively unlimited history.

History commands

Command Description
:history List all recorded commands with index numbers
:save history Save history buffer to disk immediately
:save history FILE Save history buffer to FILE
!! Re-run the last non-comment history entry
!N Re-run history entry number N

History is loaded from ~/.local/state/tgdb/history at startup. A session delimiter comment (# tgdb begins YYYY-MM-DD HH:MM:SS) is automatically prepended. Commands from the rc file are not recorded; only interactive commands are saved. History is written to disk on exit.

:highlight — customise colours

:highlight GROUP ctermfg=COLOR ctermbg=COLOR cterm=ATTRS

Groups: Statement, Type, Constant, Comment, PreProc, Normal, StatusLine, Search, IncSearch, SelectedLineArrow, ExecutingLineArrow, SelectedLineHighlight, ExecutingLineHighlight, SelectedLineBlock, ExecutingLineBlock, Breakpoint, DisabledBreakpoint, Logo, Mark, etc.

Colours: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, Brown/DarkYellow, LightGray/Gray, DarkGray, Blue/LightBlue, Green/LightGreen, Cyan/LightCyan, Red/LightRed, Magenta/LightMagenta, Yellow/LightYellow, White.

Attributes: bold, underline, reverse/inverse, standout, blink, dim, normal/NONE.

:map / :imap — key mappings

:map <F7> :step<Enter>
:map <F8> :next<Enter>
:imap <C-x> :quit<Enter>
:unmap <F7>
:iunmap <C-x>

Example ~/.config/tgdb/tgdbrc

set tabstop=4
set hlsearch
set autosourcereload
set executinglinedisplay=longarrow
set selectedlinedisplay=block
set scrollbackbuffersize=50000
set winsplit=even
highlight Statement ctermfg=Yellow cterm=bold
highlight Comment ctermfg=DarkGray
highlight Breakpoint ctermfg=Red cterm=bold
map <F5> :run<Enter>
map <F6> :continue<Enter>
map <F7> :finish<Enter>
map <F8> :next<Enter>
map <F10> :step<Enter>

Architecture

tgdb/
├── __main__.py          CLI entry point (argparse, launches app)
├── app/                 Main application package — TGDBApp + workspace tree
├── gdb_controller/      GDB bridge package — controller, MI parser, callbacks
├── source_widget/       Source code viewer package (SourceView + internal helpers)
├── gdb_widget/          GDB console package — pane, screen, scroll mode
├── command_line_bar/    Command/status bar package
├── context_menu/        Cascading workspace context-menu package
├── status_bar.py        Status bar widget
├── file_dialog/         File picker dialog package
├── config/             Configuration package (tgdbrc parser + helpers)
├── highlight_groups.py  Highlight group table and colour management
└── key_mapper.py        Key mapping with trie + timeout (KUI)

How it works

  1. Startup: TGDBApp reads $XDG_CONFIG_HOME/tgdb/tgdbrc (default ~/.config/tgdb/tgdbrc), spawns GDB via ptyprocess with dual PTYs: primary (console), secondary (MI channel).
  2. GDB I/O: Two asyncio readers:
    • Primary PTY: raw console bytes → GDB widget (with ANSI colours)
    • Secondary PTY: MI stream → parser for async records (*stopped, =thread-selected, etc.)
  3. Event-driven source updates: An AF_UNIX socketpair lets GDB's embedded Python (via gdb.events.* hooks in tgdb_pysetup.py) notify tgdb of frame changes, register writes, inferior calls, and library loads without relying on MI async records alone (GDB does not broadcast per-UI frame changes). Events are coalesced and dispatched on the asyncio loop; see Event Hooks below.
  4. Source view: When the selected frame changes (via CLI up/down, library load, or stop event), the source widget loads the file (if needed) and positions the executing-line arrow.
  5. Breakpoints: The space bar sends -break-insert file:line via MI. Breakpoint events update the gutter markers.
  6. Key mapping: All keys pass through KeyMapper which resolves user maps (:map/:imap) before dispatching.

Event Hooks

tgdb wires several GDB Python events via an AF_UNIX socketpair (created at startup and inherited into the GDB child process). Each event is encoded as a tagged line, coalesced on the asyncio loop, and dispatched to callbacks in core.py and callbacks.py. This mechanism captures frame and state changes that GDB does not broadcast over the MI channel.

Event GDB Python hook Purpose
P gdb.events.before_prompt CLI prompt about to display — refresh selected frame (catch up/down/frame N typed directly)
R<n> gdb.events.register_changed User set a register (e.g. set $rax=0x1234) — refresh register pane immediately
O/F/C gdb.events.new_objfile / free_objfile / clear_objfiles Shared libraries loaded/unloaded; invalidate any cached objfile state (currently unused; file list is lazily fetched by file dialog)
Ipre / Ipost gdb.events.inferior_call Expression trigger inferior execution (e.g. print foo()) — post refreshes locals/registers/memory since inferior ran
X gdb.events.gdb_exiting GDB's main loop shutting down — start tgdb exit promptly (avoids lag waiting for PTY EOF)

All handlers in tgdb_pysetup.py swallow write errors so a dead reader cannot stall GDB. Per-tag coalescing means a burst of N events produces a single dispatch per asyncio cycle, keeping MI load low.


Differences from cgdb

  • Uses Textual (Python TUI framework) instead of ncurses directly
  • Uses Pygments for syntax highlighting (supports many more languages than cgdb's built-in flex lexers)
  • GDB/MI parsing is done in pure Python (no C++ gdbwire library)
  • Readline integration: basic history via up/down arrows (no libreadline)
  • The --tty / Ctrl-T separate TTY feature is not yet implemented
  • Event-driven frame/state updates: Embeds Python in GDB to hook gdb.events.* (before_prompt, register_changed, inferior_call, gdb_exiting, etc.) via an inherited AF_UNIX socket, ensuring frame changes from CLI commands (up/down/frame N) update the source pane immediately, without relying on MI async records alone

About

TUI front-end for GDB

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages