Skip to content

feat: Integrate interactive Python shell into pyRevit with UI enhancements - #3480

Open
romangolev wants to merge 20 commits into
developfrom
feat/revitpythonshell-inside-pyrevit
Open

feat: Integrate interactive Python shell into pyRevit with UI enhancements#3480
romangolev wants to merge 20 commits into
developfrom
feat/revitpythonshell-inside-pyrevit

Conversation

@romangolev

Copy link
Copy Markdown
Member

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:

  • REPL + editor UI — AvalonEdit-based console with history, completion popups, syntax highlighting, plus an optional code editor pane, themed with fluent glyphs and custom title bars.
  • Multiple open modes — Modal, Modeless, Modal Editor, Modeless Editor, Docked, Docked Editor.
    Dockable variants register through f the console builds lazily on firstshow via an Idling handler. Mode is configurable via Shift+Click on the pushbutton.
  • Theme matching — detects the activlight/dark to match.
  • Runtime integration — statements execute through an ExternalEvent; pyrevit/revit/events.py now
    creates its module-level ExternalEveom the shell's REPL thread isn'talways a valid API context.
  • Dev host — pyRevitLabs.PyRevit.Sheable (excluded from CI/product build) for iterating on the shell UI outside Revit.
  • Build pipeline — BuildShellModule.cs deploys pyRevitLabs.PyRevit.Shell.dll alongside each IronPython engine.

Additional Notes

  • Dockable pane ID is shared/hardcoded between pyRevitCore.extension/startup.py and the pushbutton's script.py — keep in sync if changed.
  • Requires closing Revit before rebuilding the shell assembly (locked engine DLLs).

romangolev added 17 commits June 7, 2026 05:47
- 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.

@devloai devloai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.py to tolerate ExternalEvent.Create being 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)

Comment thread pyrevitlib/pyrevit/revit/events.py Outdated
Comment thread extensions/pyRevitCore.extension/startup.py Outdated
Comment thread dev/pyRevitLabs.PyRevit.Shell/ShellLauncher.cs Outdated
Comment thread pyrevitlib/pyrevit/revit/events.py Outdated
Comment thread dev/pyRevitLabs.PyRevit.Shell/Console/PythonConsoleHost.cs Outdated
Comment thread extensions/pyRevitCore.extension/startup.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Shell WPF-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.

Comment thread pyrevitlib/pyrevit/revit/events.py Outdated
Comment thread pyrevitlib/pyrevit/revit/events.py Outdated
Comment thread dev/pyRevitLabs.PyRevit.Shell/ShellLauncher.cs Outdated
Comment thread dev/pyRevitLabs.PyRevit.Shell/ShellLauncher.cs
Comment thread dev/pyRevitLabs.PyRevit.Shell.DevHost/Program.cs
@jmcouffin

Copy link
Copy Markdown
Contributor

Can you take a look at the last comments from copilot and devlo @romangolev and then mark as ready for review

@jmcouffin
jmcouffin marked this pull request as draft July 22, 2026 18:56
- 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.
@romangolev
romangolev marked this pull request as ready for review July 26, 2026 22:31
@romangolev
romangolev requested a review from jmcouffin July 26, 2026 22:31
@romangolev romangolev self-assigned this Jul 27, 2026
@romangolev
romangolev requested a review from sanzoghenzo August 13, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants