| name | gtk-inspector |
| description | Using GTK Inspector for debugging widget trees, CSS styling, GObject lifecycle, signal handlers, layout constraints, and rendering performance in gnostr and other nostrc GTK4 apps. Covers both interactive use via Broadway/Playwright and programmatic use in tests and debug builds.
|
| allowed-tools | Bash,Read,mcp__playwright__*,mcp__RepoPrompt__* |
| version | 1.0.0 |
GTK Inspector Debugging
GTK Inspector is a built-in interactive debugger for GTK4 applications. It provides
real-time introspection of widget trees, CSS styling, GObject properties, signal
connections, layout measurements, accessibility, and rendering performance โ all
without recompilation.
When combined with Broadway + Playwright, the LLM can interact with the inspector
programmatically: clicking buttons, reading widget properties, and diagnosing issues
in a deterministic loop.
Quick Start
Launch with Inspector
GTK_DEBUG=interactive GDK_BACKEND=broadway BROADWAY_DISPLAY=:5 \
GSETTINGS_SCHEMA_DIR=build/apps/gnostr \
build/apps/gnostr/gnostr
Architecture with Broadway
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Broadway Browser Tab โ
โ โ
โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ
โ โ App Window โ โ Inspector โ โ
โ โ โ โ Window โ โ
โ โ โโโโโโโโโโโโ โ โ โโโโโโโโโโโโ โ โ
โ โ โ Timeline โ โ โ โ Objects โ โ โ
โ โ โ Cards โ โ โ โ CSS โ โ โ
โ โ โ Composer โ โ โ โ Visual โ โ โ
โ โ โ ... โ โ โ โ A11y โ โ โ
โ โ โโโโโโโโโโโโ โ โ โ Stats โ โ โ
โ โโโโโโโโโโโโโโโโ โ โโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโ โ
โ โฒ Playwright MCP can interact โ
โ with BOTH windows โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Inspector is a separate GtkWindow rendered in the same Broadway session.
Playwright sees both windows and can interact with either โ take snapshots of the
inspector to read widget properties, click inspector buttons to navigate the tree,
and use screenshots to see visual overlays.
Inspector Panels
1. Objects Panel โ Widget Tree & Properties
The primary panel for debugging widget hierarchy and GObject state.
What it shows:
- Complete widget tree (every
GtkWidget in the application)
- Selected widget's GObject properties (name, value, type, flags)
- Property change notification (live updates when properties change)
- CSS classes applied to the selected widget
- Widget state flags (sensitive, visible, focusable, etc.)
How to use it for nostrc debugging:
# Via Broadway + Playwright:
# 1. Open Inspector
# 2. Take accessibility snapshot โ Inspector panel labels are accessible
browser_snapshot()
# 3. Look for the Objects panel in the inspector window
# 4. Navigate to find the problem widget
# The object tree mirrors the GtkWidget containment hierarchy
Key things to check:
- Widget sizing: Select a widget โ check
width-request, height-request,
hexpand, vexpand, halign, valign properties
- Visibility: Check
visible, sensitive, can-focus state flags
- Model binding: For
GtkListView, check model property to see the
GtkSelectionModel and its underlying GListModel
- Factory state: For
GtkListItemFactory, check bound items
Debugging NostrGtkTimelineView sizing (blocker #3):
1. Launch app with Inspector
2. In Inspector Objects panel, expand:
GnostrMainWindow โ GtkBox โ GtkScrolledWindow โ GtkListView
3. Select the GtkListView
4. Check properties:
- vexpand: should be TRUE
- hexpand: should be TRUE
- If a child NostrGtkNoteCardRow is visible, select it
- Check its height-request, vexpand, natural size
5. Take screenshot to see the visual highlight overlay
(Inspector highlights the selected widget with a blue border)
2. CSS Panel โ Live Style Editing
What it shows:
- All CSS rules applied to the selected widget
- Rule specificity and source (theme, app CSS, Inspector override)
- Computed style values
- CSS node tree (GTK's internal CSS model)
How to use it:
The Inspector's CSS panel has a live CSS editor. You can type CSS rules and
see them applied immediately โ without rebuilding.
# Via Broadway:
# 1. Open Inspector โ CSS panel
# 2. Type CSS to test sizing fixes:
/* Force timeline cards to respect max height */
row.note-card {
max-height: 400px;
overflow: hidden;
}
/* Debug: highlight all expanding widgets */
* {
outline: 1px solid rgba(255, 0, 0, 0.3);
}
/* Debug: show widget borders to find sizing issues */
box, scrolledwindow, listview {
border: 1px dashed blue;
}
CSS debugging workflow for widget sizing:
1. Add debug outlines via Inspector CSS panel
2. Screenshot โ identify which widget expands incorrectly
3. Select that widget in Objects panel โ check hexpand/vexpand
4. Test a fix in Inspector CSS panel (live)
5. Once working, copy the CSS rule to the app's stylesheet:
apps/gnostr/data/ui/styles/gnostr.css
6. Rebuild and verify
App CSS files for reference:
| App | CSS file | Purpose |
|---|
| gnostr | apps/gnostr/data/ui/styles/gnostr.css | App theme |
| gnostr-signer | apps/gnostr-signer/data/css/app.css | Signer theme |
| gnostr-signer | apps/gnostr-signer/data/css/high-contrast.css | Accessibility |
3. Visual Panel โ Layout & Rendering Debug
What it shows:
- Widget bounds (margins, borders, padding, content โ like browser DevTools box model)
- Baseline alignment
- Layout constraints
- Clip regions
- Widget allocation (actual pixel position + size)
How to use it for sizing issues (blocker #3):
1. Select a NostrGtkNoteCardRow in the Objects panel
2. Switch to Visual panel
3. Read the box model:
- Margin: outside spacing
- Border: drawn border
- Padding: inside spacing
- Content: actual content area
4. Compare content area size to the widget's natural size
5. If content area exceeds the ScrolledWindow, the widget
is not properly constrained
What to look for:
- Content size vastly larger than allocation โ widget not respecting parent size
- Zero margins/padding when you expect spacing โ CSS not applied
- Baseline misalignment โ mixed text/icon widgets without baseline sync
4. Accessibility Panel โ Widget Semantics
What it shows:
- Accessible role (button, label, list, listitem, etc.)
- Accessible properties (label, description, value)
- Accessible states (checked, selected, disabled, etc.)
- Accessible relations (labelledby, describedby, etc.)
Why it matters for LLM debugging:
Playwright's browser_snapshot() returns an accessibility tree โ the same data
this panel shows. If a widget isn't showing up in Playwright snapshots, this panel
tells you why (missing accessible role, invisible to AT, etc.).
5. Statistics Panel โ Rendering Performance
What it shows:
- Frame rate (FPS)
- Frame times (per-frame rendering cost)
- Number of rendered nodes
- Texture memory usage
- CSS node count
How to use it for latency debugging (blocker #5):
1. Open Inspector โ Statistics panel
2. Scroll the timeline rapidly
3. Watch frame times:
- Consistent <16ms โ smooth (60fps)
- Spikes >50ms โ main-thread stall (correlate with NDB violations)
- Steady >33ms โ too much rendering work per frame
4. Watch texture memory:
- Growing without bound โ media cache not evicting
- Spiking during scroll โ too many textures created per frame
6. Recorder Panel โ Frame-by-Frame Analysis
What it shows:
- Records render node trees for each frame
- Allows frame-by-frame playback
- Shows exactly what was rendered and why
- Identifies redundant redraws
How to use it:
1. Open Inspector โ Recorder tab
2. Click "Record"
3. Perform the problematic action (scroll, load timeline, etc.)
4. Click "Stop"
5. Step through frames:
- Identify frames with excessive render nodes
- Find widgets being redrawn unnecessarily
- Spot layout invalidation cascades
Using Inspector via Playwright (LLM-Actionable)
The Inspector window appears in the Broadway session alongside the app window.
Playwright can interact with both. Here's how the LLM can use it:
Step 1: Identify the Inspector window
# After launching with GTK_DEBUG=interactive:
browser_snapshot()
# The snapshot will show TWO windows:
# - The app window (GNostr, GNostr Signer, etc.)
# - The Inspector window ("GTK Inspector")
# Each has accessible refs you can click
Step 2: Navigate the Inspector
# Click the "Objects" tab in the inspector
browser_click(element="Objects", ref="eNN")
# Take a snapshot to see the widget tree
browser_snapshot()
# Click to expand a widget in the tree
browser_click(element="GnostrMainWindow", ref="eNN")
browser_click(element="GtkBox", ref="eNN")
browser_click(element="GtkScrolledWindow", ref="eNN")
# Take a snapshot to read the selected widget's properties
browser_snapshot()
Step 3: Read widget properties
# After selecting a widget, the properties panel shows:
browser_snapshot()
# Look for property rows like:
# width-request: -1
# height-request: -1
# hexpand: TRUE
# vexpand: TRUE
# visible: TRUE
# css-classes: ["note-card", "card"]
Step 4: Use the "Pick Widget" tool
# Click the crosshair/target icon in the Inspector toolbar
browser_click(element="Pick a widget", ref="eNN")
# Now click on a widget in the APP window
browser_click(element="<target widget in app>", ref="eNN")
# The Inspector jumps to that widget in the tree
browser_snapshot() # Read its properties
Step 5: Live CSS editing
# Click the "CSS" tab
browser_click(element="CSS", ref="eNN")
# Click the CSS text area
browser_click(element="<CSS editor area>", ref="eNN")
# Type CSS rules
browser_type(element="<CSS editor>", ref="eNN",
text="row.note-card { max-height: 400px; }")
# Take screenshot to see effect immediately
browser_take_screenshot(filename="css-test.png")
Programmatic Inspector Usage
In test code
#include <gtk/gtk.h>
gtk_window_set_interactive_debugging(TRUE);
void debug_widget_tree(GtkWidget *root) {
GtkWidget *child = gtk_widget_get_first_child(root);
while (child) {
int min_w, nat_w, min_h, nat_h;
gtk_widget_measure(child, GTK_ORIENTATION_HORIZONTAL, -1,
&min_w, &nat_w, NULL, NULL);
gtk_widget_measure(child, GTK_ORIENTATION_VERTICAL, nat_w,
&min_h, &nat_h, NULL, NULL);
g_print("Widget %s: %s min=%dx%d nat=%dx%d alloc=%dx%d\n",
G_OBJECT_TYPE_NAME(child),
gtk_widget_get_css_name(child),
min_w, min_h, nat_w, nat_h,
gtk_widget_get_width(child),
gtk_widget_get_height(child));
debug_widget_tree(child);
child = gtk_widget_get_next_sibling(child);
}
}
In GDB/LLDB sessions
gdb -batch \
-ex "break nostr_gtk_note_card_row_prepare_for_bind" \
-ex "run" \
-ex "call gtk_window_set_interactive_debugging(1)" \
-ex "continue" \
--args build-debug/apps/gnostr/gnostr
gdb -ex "break factory_bind_cb" \
-ex "run" \
-ex "call (void)g_object_get_data(row, \"css-name\")" \
-ex "call (int)gtk_widget_get_width(row)" \
-ex "call (int)gtk_widget_get_height(row)" \
-ex "call (int)gtk_widget_get_hexpand(row)" \
-ex "continue" \
--args build-debug/apps/gnostr/gnostr
GObject Lifecycle Debugging with Inspector
The Inspector's Objects panel shows all live GObjects โ not just widgets.
This is invaluable for debugging reference count issues (blocker #2).
Finding leaked objects
1. Open Inspector โ Objects panel
2. Use the search/filter to find your type (e.g., "NostrGtkNoteCardRow")
3. Count instances:
- After scrolling away from a timeline, the old NostrGtkNoteCardRow
instances should be finalized
- If the count keeps growing โ leak
4. Select an instance โ check "References" in properties:
- ref_count value
- Who holds references (if g_object_ref tracking is enabled)
Signal handler debugging
1. Select a widget in the Objects panel
2. Look at the "Signals" section in the properties pane
3. It lists ALL connected signal handlers:
- Signal name (e.g., "notify::profile")
- Handler function pointer
- Connected object / user_data
- Whether it's currently blocked
4. After unbind, the handler list should be EMPTY
(or only contain handlers from the widget itself)
5. If a handler remains after unbind โ it will cause UAF on next emit
Property change monitoring
1. Select a GObject in the Objects panel
2. Properties update LIVE as the app runs
3. Useful for debugging:
- Watch "ref-count" changing (shouldn't grow unbounded)
- Watch "visible" toggling (layout thrashing?)
- Watch model "n-items" (growing without bound?)
- Watch "content" property on event items (large strings?)
Debugging Scenarios with Inspector
Scenario 1: NostrGtkNoteCardRow expanding beyond container
Problem: Timeline cards grow to enormous heights with long content.
1. Launch: GTK_DEBUG=interactive GDK_BACKEND=broadway ...
2. browser_snapshot() โ find Inspector window
3. In Inspector, click "Pick Widget" tool
4. Click on the oversized card in the app
5. Inspector selects the NostrGtkNoteCardRow
6. Read properties:
- height-request: -1 (no explicit constraint)
- vexpand: TRUE (expanding to fill!)
- Check parent ScrolledWindow max-content-height
7. Switch to Visual panel โ read actual allocation
8. Switch to CSS panel โ add test constraint:
row.note-card { max-height: 400px; overflow: hidden; }
9. Screenshot โ verify the fix works
10. Apply to gnostr.css and rebuild
Scenario 2: Signal handler leak causing crash
Problem: Segfault after rapid scrolling (recycling).
1. Launch with Inspector + ASan build
2. Scroll slowly โ select a NostrGtkNoteCardRow in Inspector
3. Count signal handlers in the Signals section
4. Scroll rapidly (items recycle) โ select same row position
5. Count signal handlers again:
- If count is HIGHER โ handlers not disconnected on unbind
- Note which signal names are accumulating
6. Cross-reference with factory_unbind_cb:
- Which handler IDs are being tracked?
- Which are being disconnected?
- The difference = the leak
Scenario 3: Memory growth during scroll
Problem: RSS grows without bound during timeline scrolling.
1. Launch with Inspector
2. Open Statistics panel
3. Note starting texture memory and CSS node count
4. Scroll the timeline for 30 seconds
5. Check Statistics:
- Texture memory growing? โ media cache not evicting
- CSS node count growing? โ widgets not being recycled
- Frame times increasing? โ render tree growing
6. Switch to Objects panel
7. Search for "NostrGtkNoteCardRow" โ count instances
- More than visible + small buffer? โ leak
8. Search for "GdkTexture" โ count instances
- More than IMAGE_CACHE_SIZE? โ cache broken
Scenario 4: Layout performance analysis
Problem: UI stutters during layout.
1. Launch with Inspector
2. Open Recorder panel
3. Start recording
4. Perform the slow action (open timeline, scroll, resize)
5. Stop recording
6. Step through frames:
- Frames with >1000 render nodes โ too complex
- Multiple consecutive frames with full-window redraws
โ layout invalidation cascade
- Look for widgets being redrawn that shouldn't be
(e.g., header bar redrawn on every scroll)
7. Identify the widget causing invalidation
8. Fix: add `gtk_widget_set_overflow(widget, GTK_OVERFLOW_HIDDEN)`
or restructure the container hierarchy
Inspector Environment Variables
| Variable | Value | Effect |
|---|
GTK_DEBUG | interactive | Opens Inspector window on app launch |
GTK_DEBUG | actions | Log all GAction activations |
GTK_DEBUG | layout | Log layout measurements and allocations |
GTK_DEBUG | snapshot | Log render snapshot operations |
GTK_DEBUG | css | Log CSS style computation |
GTK_DEBUG | builder | Log GtkBuilder UI template loading |
GTK_DEBUG | size-request | Log size request/measure calls |
GTK_DEBUG | interactive,layout | Combine: Inspector + layout logging |
Multiple values are comma-separated:
GTK_DEBUG=interactive,layout,size-request GDK_BACKEND=broadway ...
Inspector Keyboard Shortcuts
| Shortcut | Action |
|---|
Ctrl+Shift+I | Toggle Inspector window |
Ctrl+Shift+D | Toggle Inspector (alternate) |
These work in the app window โ press them in the Broadway browser tab.
Limitations & Workarounds
| Limitation | Workaround |
|---|
| Inspector adds overhead | Only use for debugging, not perf measurement |
| Recorder generates large data | Record short (2-3 second) segments |
| Pick Widget may not work via Broadway click events | Use the Objects tree to navigate manually |
| CSS editor has no undo | Keep track of your test CSS, paste from notes |
| Some properties are read-only | Use GDB to call g_object_set() at runtime |
| Inspector can't show finalized objects | Use G_DEBUG=instance-count to track types |
Combining with Other Debug Tools
Inspector + ASan (memory debugging)
cmake -B build-asan -DCMAKE_BUILD_TYPE=Debug \
-DGNOSTR_ENABLE_ASAN=ON
GTK_DEBUG=interactive GDK_BACKEND=broadway BROADWAY_DISPLAY=:5 \
GSETTINGS_SCHEMA_DIR=build-asan/apps/gnostr \
build-asan/apps/gnostr/gnostr 2>&1 | tee /tmp/asan.log &
Inspector + NDB Violation Detection
cmake -B build-test -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON
GTK_DEBUG=interactive GDK_BACKEND=broadway BROADWAY_DISPLAY=:5 \
GSETTINGS_SCHEMA_DIR=build-test/apps/gnostr \
build-test/apps/gnostr/gnostr 2>&1 | tee /tmp/violations.log &
Inspector + GDB
gdb -ex "run" --args env \
GTK_DEBUG=interactive GDK_BACKEND=broadway BROADWAY_DISPLAY=:5 \
build-debug/apps/gnostr/gnostr
(gdb) call (int)gtk_widget_get_width($selected_widget)
(gdb) call (int)gtk_widget_get_height($selected_widget)
(gdb) call (void)g_object_get(widget, "hexpand", &expand, NULL)
Related