feat: Integrate interactive Python shell into pyRevit with UI enhancements - #3480
feat: Integrate interactive Python shell into pyRevit with UI enhancements#3480romangolev wants to merge 20 commits into
Conversation
- Create pyRevitLabs.PyRevit.Shell, an AvalonEdit-based WPF interactive Python console - Integrate shell compilation into dev scripts, labs build system, and command line utility - Add a new 'Python Shell' pulldown button under the pyRevit panel with Modal and Modeless shell launchers
Host the IronPython console as a pyRevit dockable pane so it stays open alongside the model. The pyRevitLabs.PyRevit.Shell assembly now exposes CreateDockableConsole, which builds a fully-wired IronPythonConsoleControl configured with the full pyRevit environment and marshals each statement through an ExternalEvent so Revit stays interactive while the pane is open. pyRevitCore registers the panel at startup with the same lifecycle as other pyRevit dockable panes.
Remove prebuilt pyRevitLabs.PyRevit.Shell.dll and ICSharpCode.AvalonEdit.dll from every engine folder under bin/. These are generated product binaries; the same pattern was used previously for pyRevitLabs.CommonWPF.dll.
Add BuildShellModule that builds pyRevitLabs.PyRevit.Shell for both IPY2712PR and IPY342 after BuildRuntimeModule. The csproj's DeployShell target copies pyRevitLabs.PyRevit.Shell.dll and ICSharpCode.AvalonEdit.dll into each engine folder, replacing the prebuilt binaries removed previously.
Introduce RevitThemeDetector (reflection-based so the same assembly compiles against pre-2024 Revits where UIThemeManager does not exist) and apply the detected theme to the console control and window/pane background across all three shell entry points: ShowModal, ShowModeless, and the dockable console. The dockable panel applies the same theme to its own background so the shell blends with the surrounding Revit chrome.
Update both the light and dark variants of the Python Shell pulldown icons.
Replace the Python Shell pulldown (Modal / Modeless / Dockable sub-buttons) with a single pushbutton. Shift+Click opens a small picker that persists the chosen mode in the script config; the next launch opens the shell as Modal, Modeless, or as the dockable pane accordingly. Icons move with the button. Rework the dockable pane registration so it fits the opt-in flow: - Pane now defaults to hidden; on the next Idling tick after startup the pane is re-hidden, since Revit persists DockablePane visibility across sessions and would otherwise reopen a pane the user never asked for. - The console is no longer built during Loaded. An Idling handler subscribed at registration time builds it once the pane has actually been shown, so the pyRevitLabs.PyRevit.Shell assembly is loaded on demand rather than at startup. - HOST_APP.uiapp is captured at startup (while the __revit__ builtin is still resolvable) and reused for late console creation. - Build failures now render the traceback inside the pane instead of leaving an empty surface with only a log entry.
Completion windows are created on demand by AvalonEdit, so they were always rendered with the default WPF light styling - jarring against a dark-themed console. Plumb the current theme from PythonConsoleControl through PythonConsolePad to PythonTextEditor, and have each freshly-created PythonConsoleCompletionWindow recolor its list, tooltip, and selection brushes when the dark theme is active. Light theme falls through to the default brushes.
…dows Add an EditorView (AvalonEdit text editor with Python syntax highlighting, file save and load, and F5 to run) paired with the existing IronPython console. Run sends the current selection, or the whole document, to the REPL through the same engine and dispatch path as the console-only shell. Two new shell entry points drive it: Shell.ModalEditor (Revit blocked, transaction can span several statements) and Shell.ModelessEditor (Revit stays interactive, dispatch via the existing ExternalEvent). ShellLauncher becomes generic over the window type via a new internal IShellWindow interface (ConsoleControl, ApplyTheme, SetRevitAsWindowOwner), so both the console-only and editor windows share the modal and modeless plumbing without duplication. InteractiveShellWindow now implements IShellWindow. The Python Shell button picker grows two options (Modal Editor, Modeless Editor) and the launcher script dispatches to the new entry points.
CreateDockableEditor was unusable: ConfigureEngineViaRuntime reaches InjectBuiltins, which reads the Ribbon via ComponentManager, and the Ribbon is owned by Revit's main UI thread - calling it from the REPL dispatcher raised InvalidOperationException. Marshal engine setup onto the main thread captured at launch (already the pattern for the existing shell entry points) and unwind TargetInvocationException via DescribeSetupError so the real failure (type, message, stack) shows in the REPL instead of a generic wrapper. Extract ConfigureControl so dockable and window-based shells share dispatch wiring, and add EnsureInteractiveBuiltins so reserved pyrevit builtins (e.g. __shiftclick__, __window__) get safe defaults if setup runs off-thread or fails. pyrevit.revit.events had a related bug: UI.ExternalEvent.Create needs a standard API context, but the module used to call it at import time. Importing it from the interactive shell (REPL runs statements on a non-command thread) crashed before the REPL could even start. Wrap the two top-level Create calls in try/except and replace them with lazy _get_unregisterer_extevent / _get_execute_extevent helpers that retry on first use. Wire the new variant into the UI: the Python Shell pushbutton picker grows a Docked Editor option, the launcher script recognises it, and pyRevitCore.startup now reads the same mode setting to pick CreateDockableEditor (or CreateDockableConsole) when the pane is first shown.
Mirrors ShellLauncher's modal path but drops the Revit-specific pieces (UIApplication, ExternalEvent, runtime-builtin injection) so the REPL and editor can be iterated on without starting Revit. The host references the already-deployed shell DLL from bin/netcore/engines/IPY2712PR/, is not referenced by any shipped solution or the build pipeline, and is therefore never built in CI or shipped to end users. Also wires a local-only appsettings.Development.json override into the build (gitignored) for engine/runtime tweaks during dev.
Replace the text-only toolbar items with Segoe Fluent Icons glyphs (with Win10 MDL2 fallback) plus labels, and add light/dark theme brush resources that are swapped at runtime by ApplyTheme alongside the editor and console palettes. Keeps the toolbar in sync with the rest of the shell's theme without bitmap assets.
Extract the toolbar brushes, control styles, and scrollbar templates into a shared ShellTheme resource dictionary plus a static ShellTheme.Apply helper that swaps the light/dark palette on any root. Both shell windows now merge ShellTheme.xaml, so a single Apply call keeps the editor, console, toolbar, scrollbars, and title bar in sync with the active Revit UI theme. Also drop the default chrome on InteractiveShellWindow / InteractiveEditorWindow and host a new ShellTitleBar UserControl (pyRevit icon, draggable, double-click to maximize, themed min/max/close buttons) so the borderless windows follow the shell palette instead of the OS caption. Stock overflow on the toolbar is collapsed on Loaded since the shell never overflows.
There was a problem hiding this comment.
PR Summary:
- Adds a full RevitPythonShell-style interactive Python shell (
pyRevitLabs.PyRevit.Shell) with an AvalonEdit-based REPL/editor, history, completion, syntax highlighting and theme matching. - Ships a new "Python Shell" pushbutton on the pyRevit panel with 6 open modes (Modal/Modeless/Docked × console/editor), plus a config.py to pick the mode.
- Wires statement execution through ExternalEvent so Revit stays interactive; hardens
pyrevit/revit/events.pyto tolerateExternalEvent.Createbeing called outside a valid API context (e.g. from the shell's REPL thread). - Adds build pipeline support (
BuildShellModule.cs) to deploy the shell DLL alongside each IronPython engine, and a dev-only host for iterating outside Revit.
Review Summary:
Reviewed the C# shell assembly, the events.py hardening, and the new/edited Python startup/pushbutton scripts against this repo's guidelines (IronPython 2.7 target compatibility, black/PEP8 formatting, and general code-review checklist patterns from prior PRs — e.g. silent-failure logging in initialization paths, ExternalEvent/queue handler design, vendored-dependency licensing conventions).
The most important finding: several new/modified Python files (events.py, startup.py, and the new pushbutton script.py/config.py) were saved with a leading UTF-8 BOM, which IronPython 2.7 does not strip — this can throw a SyntaxError at import/execution time and should be fixed before merge. I also flagged a concurrency hazard in ShellLauncher.cs where a non-thread-safe Queue<Action> and a single shared AutoResetEvent are used by multiple independently-running threads (REPL execution, paste/editor "Run", and background autocomplete), a silently-dropped scheduled callback in events.py when ExternalEvent creation fails, a missing blank line between two method defs (black/PEP8), and a missing license file for the vendored console-control code (consistent with this repo's licenses/ convention for every other vendored dependency).
Overall the shell architecture itself (lazy dockable-pane build via Idling, ExternalEvent-based dispatch, theme detection) is well designed; the issues above are concrete and actionable rather than architectural concerns.
Suggestions
- Re-save events.py, startup.py, script.py, and config.py as BOM-less UTF-8. Apply
- Add a licenses/ file attributing the vendored PythonConsoleControl (Joe Moorhouse) code. Apply
⚡ Apply all quick fixes (5 quick fixes)
There was a problem hiding this comment.
Pull request overview
This PR introduces a RevitPythonShell-style interactive Python REPL (with optional AvalonEdit editor) integrated directly into pyRevit, including a new shell assembly, a ribbon pushbutton to launch it in multiple modes (modal/modeless/docked), and supporting runtime/build pipeline updates so commands execute in a valid Revit API context.
Changes:
- Adds a new
pyRevitLabs.PyRevit.ShellWPF-based interactive shell (console + optional editor) with theme matching and ExternalEvent-based dispatch for modeless/docked scenarios. - Registers a new dockable panel at startup and adds a new “Python Shell” pushbutton with a Shift+Click configuration UI.
- Extends dev/build tooling to build and deploy the shell per IronPython fork and to expose runtime engine configuration for interactive usage.
Reviewed changes
Copilot reviewed 50 out of 54 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| pyrevitlib/pyrevit/revit/events.py | Makes ExternalEvent creation tolerant to non-API import contexts and defers creation/raising logic. |
| extensions/pyRevitCore.extension/startup.py | Registers a dockable “pyRevit Python Shell” panel and lazily builds the hosted shell control on first show. |
| extensions/pyRevitCore.extension/PythonShellDockablePanel.xaml | Adds a minimal dockable pane host surface for the shell control. |
| extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Python Shell.pushbutton/script.py | Adds pushbutton script to open the shell in modal/modeless/docked (+ editor) modes. |
| extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Python Shell.pushbutton/config.py | Adds Shift+Click configuration UI for selecting shell open mode. |
| extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Python Shell.pushbutton/bundle.yaml | Defines UI metadata (title/tooltip/context) for the new pushbutton. |
| extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/bundle.yaml | Adds “Python Shell” into the pyRevit panel layout. |
| dev/scripts/configs.py | Adds a dev path constant for the shell project. |
| dev/pyRevitLabs.PyRevit.Shell/Themes/ShellTheme.xaml | Introduces shared theme brushes and control styles for shell UI chrome/scrollbars. |
| dev/pyRevitLabs.PyRevit.Shell/Themes/ShellTheme.cs | Adds runtime theme brush swapping for light/dark palettes. |
| dev/pyRevitLabs.PyRevit.Shell/ShellTitleBar.xaml.cs | Implements custom draggable title bar behavior for borderless shell windows. |
| dev/pyRevitLabs.PyRevit.Shell/ShellTitleBar.xaml | Defines the title bar UI with minimize/maximize/close buttons and icon. |
| dev/pyRevitLabs.PyRevit.Shell/ShellLauncher.cs | Implements shell window/control creation and Revit-context dispatching via ExternalEvent. |
| dev/pyRevitLabs.PyRevit.Shell/Shell.cs | Exposes public entry points for launching shell windows and creating dockable controls. |
| dev/pyRevitLabs.PyRevit.Shell/RevitThemeDetector.cs | Detects Revit’s current UI theme via reflection for compatibility across versions. |
| dev/pyRevitLabs.PyRevit.Shell/pyRevitLabs.PyRevit.Shell.csproj | Adds the new shell project targeting net48 and net8.0-windows with embedded resources. |
| dev/pyRevitLabs.PyRevit.Shell/InteractiveShellWindow.xaml.cs | Hosts the REPL-only shell window and applies theme/owner behavior. |
| dev/pyRevitLabs.PyRevit.Shell/InteractiveShellWindow.xaml | Defines the shell window (borderless) layout for the console-only variant. |
| dev/pyRevitLabs.PyRevit.Shell/InteractiveEditorWindow.xaml.cs | Hosts the editor+REPL window and applies theme/owner behavior. |
| dev/pyRevitLabs.PyRevit.Shell/InteractiveEditorWindow.xaml | Defines the shell window layout for the editor+console variant. |
| dev/pyRevitLabs.PyRevit.Shell/EditorView.xaml.cs | Implements AvalonEdit editor actions (open/save/run) and theme synchronization with console. |
| dev/pyRevitLabs.PyRevit.Shell/EditorView.xaml | Defines editor+console UI (toolbar, editor, splitter, console). |
| dev/pyRevitLabs.PyRevit.Shell/Directory.Build.targets | Adds per-IronPython-fork build configurations and deploys shell + AvalonEdit into engine folders. |
| dev/pyRevitLabs.PyRevit.Shell/Console/Resources/Python.xshd | Adds a light syntax-highlighting definition for Python. |
| dev/pyRevitLabs.PyRevit.Shell/Console/Resources/Python-Dark.xshd | Adds a dark syntax-highlighting definition for Python. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonTextEditor.cs | Adapts/extends console editor behaviors including themed completion popups. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonOutputStream.cs | Writes engine output into the console editor and decodes unicode escapes. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonEditingCommandHandler.cs | Implements editing command behaviors (cut/copy/delete) for the console editor. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonConsolePad.cs | Wraps console host/control and applies theme settings (including completion theme). |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonConsoleHost.cs | Hosts the IronPython console runtime and wires output/engine initialization. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonConsoleHighlightingColorizer.cs | Adds a custom colorizer that distinguishes input vs output lines. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonConsoleControl.xaml.cs | Implements WPF control hosting and theme/highlighting application logic. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonConsoleControl.xaml | Defines the WPF wrapper control for the embedded console editor. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonConsoleCompletionWindow.cs | Implements completion UI and applies dark theme styling to list/tooltip. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonConsoleCompletionDataProvider.cs | Provides completion items/descriptions, including fork-agnostic iteration behavior. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonConsole.cs | Implements the interactive console behaviors, history, dispatching, and statement execution. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonCompletionData.cs | Defines AvalonEdit completion item entries. |
| dev/pyRevitLabs.PyRevit.Shell/Console/CommandLineHistory.cs | Implements command history navigation for the console. |
| dev/pyRevitLabs.PyRevit.Shell.DevHost/README.md | Documents the dev-only host app for iterating on the shell outside Revit. |
| dev/pyRevitLabs.PyRevit.Shell.DevHost/pyRevitShellDevHost.csproj | Adds a dev-only WinExe project referencing deployed engine DLLs with optional pre-build shell build. |
| dev/pyRevitLabs.PyRevit.Shell.DevHost/Program.cs | Adds a standalone host executable that launches the shell UI outside Revit for UI iteration. |
| dev/pyRevitLabs.PyRevit.Runtime/IronPythonEngine.cs | Refactors builtins injection into a reusable method for interactive shell configuration. |
| dev/pyRevitLabs.PyRevit.Runtime/InteractiveEngine.cs | Adds a runtime entry point for configuring an interactive engine like a normal pyRevit run. |
| dev/pyrevit.py | Adds a pyrevit build shell CLI target. |
| dev/_labs.py | Implements the build_shell CLI command to build per-fork configurations. |
| dev/_build.py | Ensures shell build runs as part of the dev “build binaries” sequence. |
| build/Program.cs | Registers the new BuildShellModule into the build pipeline. |
| build/Modules/BuildShellModule.cs | Adds a build module to build/deploy the shell after runtime builds. |
| build/Helpers/PyRevitPaths.cs | Adds a path helper for the shell project. |
| build/Build.csproj | Copies Development appsettings to output (for local overrides). |
| .gitignore | Ignores local-only build/appsettings.Development.json. |
|
Can you take a look at the last comments from copilot and devlo @romangolev and then mark as ready for review |
- ShellLauncher: replace the AutoResetEvent completion signal with a per-request TaskCompletionSource so a Denied/TimedOut Raise() can cancel the in-flight request instead of leaving the caller blocked forever; guard GetCurrentDispatcher/RunOnDispatcher against threads with no WPF dispatcher. - events.py: check the ExternalEventRequest response from Raise() and discard the scheduled callback on Denied/TimedOut or exception, rather than assuming the event always fires. - Python Shell button: show a clear alert (instead of an unhandled exception) when the shell DLL isn't deployed for the active engine or fails to load. - DevHost: also show the "not built yet" message in a MessageBox, since console output isn't visible when launched by double-click. - Add AvalonEdit and RevitPythonShell license files for the vendored console code, and drop stray BOM/trailing-whitespace from the files touched above.
Description
Adds a RevitPythonShell-style interactive Python shell directly into pyRevit, so users can run Python statements against the live Revit API without leaving Revit. Ships as a new pyRevitLabs.PyRevit.Shell assembly plus a Python Shell pushbutton on the pyRevit panel.
Key pieces:
Dockable variants register through f the console builds lazily on firstshow via an Idling handler. Mode is configurable via Shift+Click on the pushbutton.
creates its module-level ExternalEveom the shell's REPL thread isn'talways a valid API context.
Additional Notes