imgui_debugger
Standalone, configurable debug panels for imgui-bundle apps
install ·
panels ·
one menu ·
inspector ·
style editor ·
examples ·
configuration ·
issues
About
Every debugging tool imgui offers normally hangs off the demo window's Tools menu. This package unbundles them: each tool is an independent, configurable panel you can drop into your own app, on your own menu, with your own shortcuts — plus a live variable inspector and a style editor whose Save and Load are yours.
Nothing here needs the demo window, and nothing here depends on anything else here: import one panel, or take the whole set.
| panel | what it shows | comes from |
|---|---|---|
Debugger |
every variable your widget can see, by scope, live | this package |
StyleEditor |
sizes, colors, rendering, with pluggable Save / Load | this package |
MetricsPanel |
windows, draw lists, viewports, internal state | imgui |
DebugLogPanel |
focus, nav, docking and IO events as they happen | imgui |
IdStackPanel |
what an item's id is built from, for id collisions | imgui |
AboutPanel |
version and the build's enabled features | imgui |
UserGuidePanel |
imgui's built-in control reference | imgui |
DemoPanel |
the demo window, if you ever want it — opt-in only | imgui |
Built for widgets like the ones in mbo_utilities and masknmf-toolbox, but it knows nothing about them — a target is any Python object.
Install
pip install imgui_debugger
The only dependency is imgui-bundle (which provides imgui, hello_imgui,
immapp and the FontAwesome icon font).
The panels
Every panel — including ones you write — has the same four-part surface:
panel.visible # bool you can set, bind, or persist
panel.menu_item() # a checked menu entry bound to visible
panel.render() # the body, at the current cursor, no window
panel.render_window() # the body in its own window; polls the hotkey
so wiring one into a host app is two lines:
def draw_menu(self):
if imgui.begin_menu("View"):
self.editor.menu_item() # or menu_item("Theme", "Ctrl+T")
imgui.end_menu()
def draw(self):
...
self.editor.render_window()
render() is there for when you would rather host the body yourself — in a tab,
a dock node, or a sidebar you already own. imgui's own windows (MetricsPanel
and friends) are window-only and say so by raising from render().
Configuration is a dataclass per panel, all sharing PanelConfig:
from imgui_bundle import imgui
from imgui_debugger import Hotkey, StyleEditor, StyleEditorConfig
editor = StyleEditor(StyleEditorConfig(
title="Theme", # window title + menu label
visible=False, # starts closed
hotkey=Hotkey(imgui.Key.t, ctrl=True), # Ctrl+T toggles it; the menu says so
window_size=(520, 700),
show_sizes=False, # StyleEditor's own knobs
on_save=my_app.save_style,
))
To show one on its own, with no host app at all:
from imgui_debugger import run_panel
run_panel(StyleEditor())
run_panel(MetricsPanel())
Writing your own panel is a subclass and a render:
from imgui_debugger import Panel, PanelConfig
class Timings(Panel):
config_class = PanelConfig
def render(self):
imgui.text(f"{imgui.get_io().framerate:.0f} fps")
One menu for everything
DebugTools is an ordered set of panels with one menu and one per-frame call:
from imgui_debugger import DebugTools, Hotkey
self.tools = DebugTools.default(self, menu_label="Debug")
self.tools["Debugger"].config.hotkey = Hotkey(imgui.Key.f12)
def draw_menu(self):
self.tools.draw_menu() # or draw_menu_items() inside your own menu
def draw(self):
...
self.tools.render() # every open panel, every hotkey
DebugTools.default() gives the inspector, the style editor, metrics, the debug
log and the ID stack tool, all starting closed. The demo window is deliberately
left out. The set is editable — add, remove, tools["title"], show_all,
hide_all, visible() — so you can swap the style editor for one configured
your way, or register a panel of your own beside them.
Variable inspector
Point it at a widget and it draws every variable that widget can see, grouped by scope, in a collapsible tree that re-reads its values every frame:
- instance — the object's own
__dict__and__slots__ - properties —
propertydescriptors, evaluated live; one that raises shows the exception instead of blanking the panel - class — class attributes from the whole MRO
- locals / globals — the call frame you captured, so you can follow a draw method's own variables
- imgui — io, mouse, keyboard, the window rect and the style metrics that explain most layout bugs
- watches — anything else you promote to a top-level scope
Leaves that are a bool, number, string or color tuple get an inline editor that writes straight back onto the object.
from imgui_debugger import attach
class RoiWidget:
def __init__(self):
self.threshold = 0.4
self.debugger = attach(self, title="ROIs widget")
def update(self): # your per-frame draw
...
self.debugger.capture() # follow this method's locals
self.debugger.render_window()
Or inspect one object with no host app:
from imgui_debugger import run_debugger
run_debugger({"fs": 9.6, "dz": 5.0, "planes": [1, 2, 3]})
Scopes
Debugger.scopes() returns them in display order: the target's
instance / properties / class, then your watches, then locals /
globals, then imgui.
| scope | source | writable |
|---|---|---|
instance |
vars(obj) + __slots__ |
yes |
properties |
property / cached_property on the MRO |
only with an fset |
class |
class attributes, no methods or descriptors | yes |
locals |
the captured frame's f_locals |
no (writes do not stick) |
globals |
the captured frame's f_globals |
yes |
imgui |
io, mouse, keyboard, window, style |
no |
| watches | watch(name, value_or_callable) |
depends on the value |
A watch takes a value or a zero-argument callable; the callable is re-read every frame, so it survives the attribute being reassigned:
dbg.watch("metadata", lambda: self.metadata, role="prop")
dbg.watch("fps", lambda: {"now": imgui.get_io().framerate}, role="runtime")
watch_all(obj, ["metadata", "indices"]) does the same for several attributes
in one call.
Toolbar
| control | what it does |
|---|---|
| filter | case-insensitive match over names and leaf values, recursing into children (bounded to 6 levels and 64 items per container, memoized per filter string) |
| expand / collapse | force every node open or shut for one frame |
| private | include _name attributes |
| edit | turn the inline editors off and read only |
Style editor
StyleEditor is the imgui demo's style editor with its Save Ref / Revert Ref /
Export buttons replaced by two of yours. imgui.show_style_editor() cannot be
drawn without those buttons, and they only manage an in-memory reference style —
nothing they do touches disk — so this draws the tabs itself.
from imgui_debugger import StyleEditor, StyleEditorConfig
editor = StyleEditor(StyleEditorConfig(
title="Theme",
visible=False,
save_label="Save to settings",
load_label="Load from settings",
on_save=my_app.save_style, # on_save(data: dict)
on_load=my_app.load_style, # on_load() -> dict | None
))
With no on_save / on_load, the buttons fall back to reading and writing
config.path as JSON (~/.imgui_debugger/style.json when that is unset too).
on_load returning None is a cancel — that is how a native file dialog the
user dismissed reports back. Errors from either hook land on the editor's status
line instead of raising inside a frame.
The body is configurable: show_sizes, show_colors and show_rendering pick
the tabs (one tab alone is drawn without a tab bar), show_font_selector and
show_style_selector add imgui's own pickers, size_groups replaces the Sizes
tab's fields entirely, and extra_draw(editor) slots your own controls under the
toolbar. Presets apply style_colors_dark / _light / _classic; Revert
restores the style as it was when the editor was constructed.
The serialization is usable on its own:
| function | does |
|---|---|
style_to_dict(style=None) |
{"sizes": {...}, "colors": {name: [r,g,b,a]}} |
apply_style_dict(data, style=None) |
writes it back, returns how many fields landed |
save_style(path=None, style=None) |
style_to_dict to JSON |
load_style(path=None, style=None) |
JSON to apply_style_dict |
Colors are keyed by imgui's own names (Text, FrameBg, ...), and unknown keys
are ignored, so a file written against an older imgui still loads. Two fields are
deliberately not serialized: font_scale_dpi and font_size_base are derived
from the screen at runtime, and restoring another machine's values resizes every
font for the wrong display.
Examples
See all examples in examples/.
| name | file | what it shows |
|---|---|---|
| debug_minimal | debug_minimal.py |
one-shot window over a settings dataclass |
| debug_widget | debug_widget.py |
a widget that owns its inspector, captures its own locals, and toggles it with F12 |
| debug_edge_window | debug_edge_window.py |
a fastplotlib EdgeWindow, the widget shape pml_utilities and masknmf-toolbox use |
| style_editor_menu | style_editor_menu.py |
the style editor alone, on a menu, saving into the app's own settings file |
| debug_tools_menu | debug_tools_menu.py |
the whole set behind one menu, with hotkeys and a replaced style editor |
Configuration reference
PanelConfig — every panel takes these
| field | default | purpose |
|---|---|---|
title |
per panel | window title and default menu label |
visible |
True |
whether it starts open |
window_id |
"" |
stable imgui id suffix; the class name when empty, so renaming the title keeps the saved layout |
window_size |
(0, 0) |
first-use size; (0, 0) sizes to content |
window_pos |
None |
first-use position |
window_flags |
0 |
imgui.WindowFlags_ bits |
closable |
True |
draw the close button and clear visible with it |
shortcut |
"" |
menu shortcut text; the hotkey's text when empty |
hotkey |
None |
Hotkey(key, ctrl=, shift=, alt=) that toggles the panel |
theme |
Theme.dark() |
palette for what the panel draws itself |
DebuggerConfig — plus the above
| field | default | purpose |
|---|---|---|
target |
None |
the object whose scopes come first |
private |
False |
show _name attributes |
properties |
True |
show the properties scope and expand nested properties |
class_attrs |
True |
show the class scope |
editable |
True |
inline editors for writable leaves |
show_frame |
True |
show locals / globals |
show_runtime |
True |
show the live imgui scope |
max_depth |
8 |
deepest level the tree expands |
max_items |
200 |
rows per container before "+N more" |
value_col |
0.0 |
pixel column values align at; 0 packs them after the name |
show_toolbar |
True |
draw the filter box and toggles |
show_title |
True |
draw the title line inside the body |
os_window_title, resizable, ini_path, assets_folder |
— | one-shot run_debugger only |
StyleEditorConfig — plus PanelConfig
| field | default | purpose |
|---|---|---|
on_save |
None |
on_save(data); falls back to writing path |
on_load |
None |
on_load() -> dict | None; falls back to reading path |
path |
None |
file used by the fallback Save / Load |
save_label, load_label |
"Save", "Load" |
button text |
show_presets, show_revert |
True |
the preset buttons and Revert |
show_sizes, show_colors, show_rendering |
True |
which tabs to draw |
show_font_selector, show_style_selector |
False |
imgui's own pickers above the tabs |
size_groups |
None |
replace the Sizes tab's field groups |
extra_draw |
None |
extra_draw(editor) under the toolbar |
attach(target, **kwargs) and run_debugger(target, **kwargs) take any
DebuggerConfig field as a keyword argument.
Files on disk
Everything the library writes lives under ~/.imgui_debugger/ (override with
the IMGUI_DEBUGGER_HOME env var):
| path | written by | purpose |
|---|---|---|
~/.imgui_debugger/debugger.ini, panel.ini |
run_debugger, run_panel |
hello_imgui window layout. Override per call; an embedding app's own ini_filename always wins. |
~/.imgui_debugger/style.json |
save_style with no path |
the fallback style file |
~/.imgui_debugger/assets/ |
you (optional) | user assets folder, added to hello_imgui's search path when the icon font cannot be resolved |
Embedded use writes nothing: render() and render_window() only draw.
Notes
- Reads are guarded. A property that raises, a
__repr__that raises, and a container that changes size mid-frame all render as a row rather than crashing the frame. - Big containers are capped, not truncated silently: a "+N more" line says how many rows were left out.
- Editing writes through the same setter the row was built from —
setattrfor an attribute,__setitem__for a dict or list entry, the property'sfsetfor a property.
Acknowledgements
The tree rendering, the bounded memoized filter and the value formatting come from the metadata inspector in mbo_utilities at the Miller Brain Observatory. The packaging, theming and one-shot harness follow imgui_data_loader.
License
MIT
Metadata
Release files for imgui_debugger 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| imgui_debugger-0.2.0.tar.gz | 52.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| imgui_debugger-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 94.7 kB
Release files / imgui_debugger-0.2.0.tar.gz
| Download URL | imgui_debugger-0.2.0.tar.gz |
|---|---|
| Size | 52.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ec2995546c5fb8bd981a693cfa698ec34735094bc8930bab3c4c3eb378ea6182
|
|
BLAKE2b-256 checksum How to use checksums |
bb0b32c7de70010c61433149184ac67574105c78871b9e6639bdaecd7610324a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / imgui_debugger-0.2.0-py3-none-any.whl
| Download URL | imgui_debugger-0.2.0-py3-none-any.whl |
|---|---|
| Size | 42.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5d4c723d2e2db36caa2d95dd01cc9b22ad3552ceaca91657467c4a38df60b119
|
|
BLAKE2b-256 checksum How to use checksums |
42463239d1b1208245444f866b64c7ed3886ec0c996238dc5af9d09d2a9a2591
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|