Files
manual_slop/docs/superpowers/plans/2026-05-11-imgui-context-managers-plan.md
T

11 KiB

ImGui Context Manager Suite Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Create a context manager suite that pairs begin/end ImGui calls using Python's with statement to reduce scope pairing errors and improve AI legibility.

Architecture: Base ImGuiScope class with factory functions returning scope objects. Each scope handles begin on __enter__ and end on __exit__.

Tech Stack: Python 3.11+, imgui-bundle (Dear PyGui)


File Structure

Action Path
Create src/imgui_scopes.py
Create tests/test_imgui_scopes.py

Task 1: Create src/imgui_scopes.py

Files:

  • Create: src/imgui_scopes.py

  • Step 1: Write the module

from __future__ import annotations
import imgui_bundle


class ImGuiScope:
    def __init__(self, begin_fn, end_fn, *args, **kwargs):
        self._begin_fn = begin_fn
        self._end_fn = end_fn
        self._args = args
        self._kwargs = kwargs
        self._opened: bool | tuple = False
        self._entered = False

    def __enter__(self):
        result = self._begin_fn(*self._args, **self._kwargs)
        if isinstance(result, tuple):
            self._opened = result[0]
        else:
            self._opened = result
        self._entered = bool(self._opened)
        return self._opened

    def __exit__(self, *args):
        if self._entered:
            self._end_fn()
        return False


def imgui_window(name: str, visible: bool = True, flags: int = 0):
    return ImGuiScope(imgui_bundle.imgui.begin, imgui_bundle.imgui.end, name, visible, flags)


def imgui_table(name: str, columns: int, flags: int = 0):
    return ImGuiScope(imgui_bundle.imgui.begin_table, imgui_bundle.imgui.end_table, name, columns, flags)


def imgui_menu_bar():
    return ImGuiScope(imgui_bundle.imgui.begin_menu_bar, imgui_bundle.imgui.end_menu_bar)


def imgui_menu(label: str):
    return ImGuiScope(imgui_bundle.imgui.begin_menu, imgui_bundle.imgui.end_menu, label)


def imgui_child(id_str: str, width: float = 0, height: float = 0, flags: int = 0):
    return ImGuiScope(imgui_bundle.imgui.begin_child, imgui_bundle.imgui.end_child, id_str, width, height, flags)


def imgui_group():
    return ImGuiScope(imgui_bundle.imgui.begin_group, imgui_bundle.imgui.end_group)


def imgui_popup(id_str: str):
    return ImGuiScope(imgui_bundle.imgui.begin_popup, imgui_bundle.imgui.end_popup, id_str)


def imgui_tooltip():
    return ImGuiScope(imgui_bundle.imgui.begin_tooltip, imgui_bundle.imgui.end_tooltip)


def imgui_clipper(count: int):
    return ImGuiScope(
        lambda n: imgui_bundle.imgui.listing_builder.begin_clipper(n, -1),
        imgui_bundle.imgui.listing_builder.end_clipper,
        count
    )


def node_editor_scope(name: str):
    ed = imgui_bundle.imgui_node_editor
    return ImGuiScope(ed.begin, ed.end, name)
  • Step 2: Verify syntax

Run: uv run python -c "import ast; ast.parse(open('src/imgui_scopes.py').read())" Expected: No output (parse successful)

  • Step 3: Commit
git add src/imgui_scopes.py
git commit -m "feat(gui): Add ImGuiScope base class and scope helpers"

Task 2: Create tests/test_imgui_scopes.py

Files:

  • Create: tests/test_imgui_scopes.py

  • Depends on: Task 1

  • Step 1: Write tests

from __future__ import annotations
from unittest.mock import patch, MagicMock
import pytest


class TestImGuiScope:
    def test_enter_calls_begin_with_args(self):
        from src.imgui_scopes import ImGuiScope
        mock_begin = MagicMock(return_value=True)
        mock_end = MagicMock()
        scope = ImGuiScope(mock_begin, mock_end, "arg1", kwarg="val")
        with scope:
            pass
        mock_begin.assert_called_once_with("arg1", kwarg="val")

    def test_exit_calls_end_when_entered(self):
        from src.imgui_scopes import ImGuiScope
        mock_begin = MagicMock(return_value=True)
        mock_end = MagicMock()
        scope = ImGuiScope(mock_begin, mock_end)
        with scope:
            pass
        mock_end.assert_called_once()

    def test_exit_does_not_call_end_when_not_entered(self):
        from src.imgui_scopes import ImGuiScope
        mock_begin = MagicMock(return_value=False)
        mock_end = MagicMock()
        scope = ImGuiScope(mock_begin, mock_end)
        with scope:
            pass
        mock_end.assert_not_called()

    def test_enter_returns_opened_value(self):
        from src.imgui_scopes import ImGuiScope
        mock_begin = MagicMock(return_value=True)
        mock_end = MagicMock()
        scope = ImGuiScope(mock_begin, mock_end)
        result = scope.__enter__()
        assert result is True

    def test_enter_handles_tuple_return(self):
        from src.imgui_scopes import ImGuiScope
        mock_begin = MagicMock(return_value=(True, "extra"))
        mock_end = MagicMock()
        scope = ImGuiScope(mock_begin, mock_end)
        with scope:
            pass
        mock_end.assert_called_once()


class TestImguiWindow:
    def test_imgui_window_returns_scope(self):
        from src.imgui_scopes import imgui_window
        with patch("src.imgui_scopes.imgui_bundle.imgui.begin", return_value=True) as mock_begin:
            with patch("src.imgui_scopes.imgui_bundle.imgui.end") as mock_end:
                scope = imgui_window("Test")
                assert hasattr(scope, "__enter__")
                assert hasattr(scope, "__exit__")


class TestImguiTable:
    def test_imgui_table_returns_scope(self):
        from src.imgui_scopes import imgui_table
        with patch("src.imgui_scopes.imgui_bundle.imgui.begin_table", return_value=True) as mock_begin:
            with patch("src.imgui_scopes.imgui_bundle.imgui.end_table") as mock_end:
                scope = imgui_table("TestTable", 3)
                with scope:
                    pass
                mock_begin.assert_called_once()
                mock_end.assert_called_once()


class TestImguiMenuBar:
    def test_imgui_menu_bar_no_args(self):
        from src.imgui_scopes import imgui_menu_bar
        with patch("src.imgui_scopes.imgui_bundle.imgui.begin_menu_bar", return_value=True) as mock_begin:
            with patch("src.imgui_scopes.imgui_bundle.imgui.end_menu_bar") as mock_end:
                scope = imgui_menu_bar()
                with scope:
                    pass
                mock_begin.assert_called_once_with()
                mock_end.assert_called_once()


class TestImguiMenu:
    def test_imgui_menu_returns_scope(self):
        from src.imgui_scopes import imgui_menu
        with patch("src.imgui_scopes.imgui_bundle.imgui.begin_menu", return_value=True) as mock_begin:
            with patch("src.imgui_scopes.imgui_bundle.imgui.end_menu") as mock_end:
                scope = imgui_menu("File")
                with scope:
                    pass
                mock_begin.assert_called_once()
                mock_end.assert_called_once()


class TestImguiChild:
    def test_imgui_child_returns_scope(self):
        from src.imgui_scopes import imgui_child
        with patch("src.imgui_scopes.imgui_bundle.imgui.begin_child", return_value=True) as mock_begin:
            with patch("src.imgui_scopes.imgui_bundle.imgui.end_child") as mock_end:
                scope = imgui_child("child_id", 100, 200)
                with scope:
                    pass
                mock_begin.assert_called_once()
                mock_end.assert_called_once()


class TestImguiGroup:
    def test_imgui_group_no_args(self):
        from src.imgui_scopes import imgui_group
        with patch("src.imgui_scopes.imgui_bundle.imgui.begin_group", return_value=True) as mock_begin:
            with patch("src.imgui_scopes.imgui_bundle.imgui.end_group") as mock_end:
                scope = imgui_group()
                with scope:
                    pass
                mock_begin.assert_called_once()
                mock_end.assert_called_once()


class TestImguiPopup:
    def test_imgui_popup_returns_scope(self):
        from src.imgui_scopes import imgui_popup
        with patch("src.imgui_scopes.imgui_bundle.imgui.begin_popup", return_value=True) as mock_begin:
            with patch("src.imgui_scopes.imgui_bundle.imgui.end_popup") as mock_end:
                scope = imgui_popup("popup_id")
                with scope:
                    pass
                mock_begin.assert_called_once()
                mock_end.assert_called_once()


class TestImguiTooltip:
    def test_imgui_tooltip_no_args(self):
        from src.imgui_scopes import imgui_tooltip
        with patch("src.imgui_scopes.imgui_bundle.imgui.begin_tooltip", return_value=True) as mock_begin:
            with patch("src.imgui_scopes.imgui_bundle.imgui.end_tooltip") as mock_end:
                scope = imgui_tooltip()
                with scope:
                    pass
                mock_begin.assert_called_once()
                mock_end.assert_called_once()


class TestNodeEditorScope:
    def test_node_editor_scope_returns_scope(self):
        from src.imgui_scopes import node_editor_scope
        mock_ed = MagicMock()
        mock_ed.begin = MagicMock(return_value=True)
        mock_ed.end = MagicMock()
        with patch("src.imgui_scopes.imgui_bundle.imgui_node_editor", mock_ed):
            scope = node_editor_scope("TestNode")
            with scope:
                pass
            mock_ed.begin.assert_called_once()
            mock_ed.end.assert_called_once()
  • Step 2: Run tests

Run: uv run pytest tests/test_imgui_scopes.py -v Expected: All tests PASS

  • Step 3: Commit
git add tests/test_imgui_scopes.py
git commit -m "test(gui): Add tests for ImGuiScope context managers"

Task 3: Integration Verification

Files:

  • Modify: src/gui_2.py (sample migration of 2-3 windows)

  • Depends on: Task 1

  • Step 1: Add import to gui_2.py

Add near line 1 (after existing imports):

from src.imgui_scopes import imgui_window, imgui_table, imgui_menu_bar, imgui_menu, imgui_child, imgui_group, imgui_popup, imgui_tooltip, imgui_clipper, node_editor_scope
  • Step 2: Migrate one window as proof-of-concept

Pick one window in _gui_func (e.g., lines 1061-1056 for "AI Settings"):

Before:

exp, opened = imgui.begin("AI Settings", self.show_windows["AI Settings"])
if exp:
    # ... content ...
    imgui.end()

After:

if imgui_window("AI Settings", self.show_windows["AI Settings"]):
    # ... content ...
# end() called automatically
  • Step 3: Run smoke test

Run: uv run pytest tests/test_gui_startup_smoke.py -v Expected: PASS (no regression from import)

  • Step 4: Commit
git add src/gui_2.py
git commit -m "refactor(gui): Migrate AI Settings window to imgui_window scope"

Self-Review Checklist

  • All scope helpers implemented with correct signatures
  • Tests mock all begin/end pairs
  • Integration test confirms no regression in gui_2.py
  • No placeholders (TBD, TODO) in any step
  • Each task commits independently