Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
32dad3e
Replace Mu's splash, window icon and Mode logo with Nu's
evangress Nov 8, 2026
008900b
Bring the running Nu to the front when it's started again
evangress Nov 8, 2026
602fa9a
Report a debugged script's real exit code
evangress Nov 8, 2026
8f09640
Say when a program has finished, and ask before quitting a running one
evangress Nov 8, 2026
b977d4d
Install the bundled packages in one uv call; drop the 8.3-path workar…
evangress Nov 8, 2026
464e4e0
Friendlier REPL, pane titles and debug inspector; keep the editor roomy
evangress Nov 8, 2026
4ce6f7f
Mode picker: Python 3 first and plainer words; click the mode's name …
evangress Nov 8, 2026
eb9e9b5
Settings dialog: no stray box on the Plugins tab, log opens at the end
evangress Nov 8, 2026
32b62ef
Make a mode's workspace folders when it's first used
evangress Nov 8, 2026
883cf2f
Move Check's micro:bit special case out of the core
evangress Nov 8, 2026
c96cd1f
Remove dead code left by Mu's removed modes
evangress Nov 8, 2026
090e859
Drop the unused nudatus and semver dependencies and the empty mu/contrib
evangress Nov 8, 2026
f4097d3
Remove image files nothing loads
evangress Nov 8, 2026
8bacbb3
Tidy Mu's repo leftovers: unused CodeQL config, Web API generator, vi…
evangress Nov 8, 2026
99c0e83
Drop obsolete translations from the catalogues
evangress Nov 8, 2026
75da699
CLAUDE.md and CHANGES.md: the cleanup, new baseline, star_imports
evangress Nov 8, 2026
85158fc
Use the keep-alive references code scanning flagged as unused
evangress Nov 8, 2026
390ecf5
Merge branch 'ux/review-findings' into cleanup/mu-leftovers
evangress Nov 8, 2026
25c33d6
Merge branch 'master' into cleanup/mu-leftovers
evangress Nov 8, 2026
2c6d1bc
Merge remote-tracking branch 'origin/master' into cleanup/mu-leftovers
evangress Nov 8, 2026
d66c71b
Merge remote-tracking branch 'origin/cleanup/mu-leftovers' into clean…
evangress Nov 8, 2026
b1a6628
CHANGES.md: restore the Removed section for this PR's cleanup
evangress Nov 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/codeql/codeql-config.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
# The bundled plugins (plugins/*) are nu-editor dependencies that uv
# resolves from the workspace; pip needs their paths given explicitly.
# micro:bit is opt-in (nu-editor's `microbit` extra) but tested here too.
- name: Install Mu dependencies
- name: Install Nu dependencies
run: |
python -m pip install --upgrade pip
pip install ./plugins/circuitpython ./plugins/pygamezero ./plugins/microbit ".[dev]"
Expand Down
8 changes: 8 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@
* The Plugins tab drew a stray empty box on the selected row (Windows 11).
* The log tab opens at the newest entries.

### Removed

* Code and files left over from Mu's removed modes: the micro:bit special case
in **Check** (modes can now declare `star_imports`, and the micro:bit plugin
does), unused REPL and file-system hooks, about 35 icon files that were never
loaded, the unused `nudatus` and `semver` dependencies, and obsolete
translations of strings Nu no longer has.

## 2.0.0

The first stable release of **Nu**, a maintained continuation of the
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Most workflows go through the `Makefile` (which delegates to `make.py`):
```
QT_QPA_PLATFORM=offscreen LANG=en_GB.utf8 uv run --no-sync pytest -q --random-order-bucket=none
```
(On a box where `/tmp` is mounted noexec, add `--basetemp=$HOME/.cache/nu-pytest/bt`: one test builds and runs a real venv in its `tmp_path`.) Current green baseline (Python 3.11 + PyQt6, with `--all-extras` so the opt-in micro:bit plugin is installed): **1638 passed, 20 skipped** (counts can shift by one between Python versions). The test session is **isolated**: before anything imports `mu`, the root `conftest.py` sets `MU_DATA_DIR`, `MU_HOME_DIR` (both honoured by `mu/config.py`) and `IPYTHONDIR` to a throwaway folder and gives the single-instance lock its own name, so tests never touch the real `~/.local/share/mu`, `~/mu_code` or IPython profile, and they run fine while a real Nu is open (`tests/test_config.py` checks this). Every test file passes on its own and in `--random-order`; autouse fixtures fail a test that really quits the `QApplication` (patch `QCoreApplication.exit`) and restore `sys.excepthook`/`threading.excepthook`. On a headless box without Mesa, Qt6's `offscreen` plugin still needs `libEGL.so.1` — put it on `LD_LIBRARY_PATH` (e.g. `apt-get download libegl1 libglvnd0 && dpkg -x` into a local dir). Lint the project's way with `uv run --no-sync python -m ruff check` and reformat with `... ruff format` (config in `pyproject.toml` `[tool.ruff]`; the gettext `_` builtin is declared there as `builtins = ["_"]`, so no env var dance is needed).
(On a box where `/tmp` is mounted noexec, add `--basetemp=$HOME/.cache/nu-pytest/bt`: one test builds and runs a real venv in its `tmp_path`.) Current green baseline (Python 3.11 + PyQt6, with `--all-extras` so the opt-in micro:bit plugin is installed): **1778 passed, 20 skipped** (counts can shift by one between Python versions). The test session is **isolated**: before anything imports `mu`, the root `conftest.py` sets `MU_DATA_DIR`, `MU_HOME_DIR` (both honoured by `mu/config.py`) and `IPYTHONDIR` to a throwaway folder and gives the single-instance lock its own name, so tests never touch the real `~/.local/share/mu` (the log follows `MU_DATA_DIR` too), `~/mu_code` or IPython profile, and they run fine while a real Nu is open (`tests/test_config.py` checks this). Every test file passes on its own and in `--random-order`; autouse fixtures fail a test that really quits the `QApplication` (patch `QCoreApplication.exit`) and restore `sys.excepthook`/`threading.excepthook`. On a headless box without Mesa, Qt6's `offscreen` plugin still needs `libEGL.so.1` — put it on `LD_LIBRARY_PATH` (e.g. `apt-get download libegl1 libglvnd0 && dpkg -x` into a local dir). Lint the project's way with `uv run --no-sync python -m ruff check` and reformat with `... ruff format` (config in `pyproject.toml` `[tool.ruff]`; the gettext `_` builtin is declared there as `builtins = ["_"]`, so no env var dance is needed).

Test layout mirrors the source tree: `tests/test_logic.py` tests `mu/logic.py`, `tests/modes/` tests `mu/modes/`, etc.; each bundled plugin's tests are in `plugins/<name>/tests/`. Shared autouse fixtures (QApplication, no autosave, temp drop-in plugins dir, plugin error handler and plugin image registry resets, the quit and exception-hook guards) and the session isolation are in the root `conftest.py`.

Expand All @@ -64,7 +64,7 @@ Mu is a **modal editor**: a fixed core of always-available features, plus a set

- **`mu/interface/`** — the Qt view layer. `interface/main.py` defines the **`Window`** class (the `view` the `Editor` drives). `editor.py` is the Scintilla-based text widget, `panes.py` the REPL/process-output/plotter panes, `dialogs.py` dialogs, `themes.py` the day/night/contrast stylesheets, `widgets.py` small shared widgets.

- **`mu/modes/`** — every mode subclasses **`BaseMode`** in `modes/base.py` (MicroPython-family modes subclass `MicroPythonMode`, which adds REPL/serial plumbing). A mode declares `name`, `description`, `icon`, and implements `actions()` (toolbar buttons), `api()` (autocomplete/calltip data), `workspace_dir()`, etc.; `uses_user_environment` (admin dialog env/packages tabs) and `ensure_workspace_assets(wd)` (called for every mode at startup) replace what used to be hardcoded per mode name — the core must not name plugin modes. New modes should be plugins (`docs/plugins.md`, template `plugins/example/`); a core mode is registered in `app.py` and `modes/__init__.py`. `modes/api/` holds generated API stubs (excluded from ruff, as are `plugins/*/nu_*/api`).
- **`mu/modes/`** — every mode subclasses **`BaseMode`** in `modes/base.py` (MicroPython-family modes subclass `MicroPythonMode`, which adds REPL/serial plumbing). A mode declares `name`, `description`, `icon`, and implements `actions()` (toolbar buttons), `api()` (autocomplete/calltip data), `workspace_dir()`, etc.; `uses_user_environment` (admin dialog env/packages tabs), `ensure_workspace_assets(wd)` (called the first time the mode is used in a session) and `star_imports` (module -> names its `import *` brings in, for Check) replace what used to be hardcoded per mode name — the core must not name plugin modes. New modes should be plugins (`docs/plugins.md`, template `plugins/example/`); a core mode is registered in `app.py` and `modes/__init__.py`. `modes/api/` holds generated API stubs (excluded from ruff, as are `plugins/*/nu_*/api`).

- **`mu/virtual_environment.py`** — Mu manages a **separate "user" virtualenv** (the `venv` singleton at module bottom) into which it installs third-party packages requested from the UI, isolated from Mu's own environment. Since Phase 4a it is driven by **uv**: `find_uv()` locates the binary (`MU_UV` → `uv` on `PATH` → the bundled `uv` wheel), `create_venv` builds it from the CPython bundled in installers (`mu/wheels/python/`, extracted once by `ensure_bundled_python` into `DATA_DIR/python/`; builds an upgrade left behind are deleted at startup by `remove_old_bundled_pythons` unless the env's `pyvenv.cfg` points into them) with `--seed --no-index`, so first launch is offline (dev checkouts without the bundle fall back to `uv venv --python <X.Y> --seed`), `Uv` wraps `uv pip`/`uv run` via `QProcess`, and `activation_vars()` activates the env for spawned user code (Run and the Jupyter kernel). Since Phase 4b (part 1) the env is **visible**: every uv command goes through `Uv` (`run_blocking`/`run`/`run_subprocess`/`script_command`), which records it in the GUI-free `mu/command_log.py` ("What Nu ran" on the admin dialog's Environment tab, and INFO in the log) — don't call uv around it; the status bar shows `Environment: <name> · Python X.Y` in modes with `uses_user_environment`; and `mu/script_metadata.py` (PEP 723) makes the Python 3 Run use `uv run --python <env python> --script <file>` for files with a `# /// script` block (see `docs/environment.md`). Bundled wheels live in `mu/wheels/` (`python -m mu.wheels --package` fetches them for installers): `wheels.baseline_packages()` = the core's `mode_packages` (ipykernel) + each installed plugin's `learner-env` extra, read from package metadata (`plugin_packages()`), never by importing the plugin.

Expand Down
14 changes: 3 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ GREP_T_FLAG := $(shell test $$(uname) = Linux && echo -T)

all:
@echo "\nThere is no default Makefile target right now. Try:\n"
@echo "make run - run the local development version of Mu."
@echo "make run - run the local development version of Nu."
@echo "make clean - reset the project and remove auto-generated assets."
@echo "make lint - run the ruff code checker."
@echo "make test - run the test suite."
Expand All @@ -18,12 +18,11 @@ all:
@echo "make docs-serve - serve the documentation locally with live reload."
@echo "make translate_begin LANG=xx_XX - create/update a mu.po file for translation."
@echo "make translate_done LANG=xx_XX - compile translation strings in mu.po to mu.mo file."
@echo "make translate_test LANG=xx_XX - run translate_done and launch Mu in the given LANG."
@echo "make translate_test LANG=xx_XX - run translate_done and launch Nu in the given LANG."
@echo "make translate_status - show how complete each translation is."
@echo "make win64 - build a 64bit Windows MSI installer (Briefcase)."
@echo "make macos - build a macOS native application (Briefcase)."
@echo "make linux - build a Linux AppImage (Briefcase)."
@echo "make video - create an mp4 video representing code commits.\n"

clean:
rm -rf build
Expand All @@ -34,8 +33,6 @@ clean:
rm -rf site
rm -rf .pytest_cache
rm -rf lib
rm -rf *.mp4
rm -rf .git/avatar/*
find . \( -name '*.py[co]' -o -name dropin.cache \) -delete
find . \( -name '*.bak' -o -name dropin.cache \) -delete
find . \( -name '*.tgz' -o -name dropin.cache \) -delete
Expand All @@ -45,7 +42,7 @@ clean:

run: clean
ifeq ($(VIRTUAL_ENV),)
@echo "\n\nCannot run Mu. Your Python virtualenv is not activated."
@echo "\n\nCannot run Nu. Your Python virtualenv is not activated."
else
python run.py
endif
Expand Down Expand Up @@ -123,8 +120,3 @@ linux: check
@echo "\nBuilding Linux AppImage (Briefcase)."
python make.py linux

video: clean
@echo "\nFetching contributor avatars."
python utils/avatar.py
@echo "\nMaking video of source commits."
gource --user-image-dir .git/avatar/ --title "The Making of Mu" --logo docs/icon_small.png --font-size 24 --file-idle-time 0 --key -1280x720 -s 0.1 --auto-skip-seconds .1 --multi-sampling --stop-at-end --hide mouse,progress --output-ppm-stream - --output-framerate 30 | ffmpeg -y -r 30 -f image2pipe -vcodec ppm -i - -b 65536K movie.mp4
6 changes: 5 additions & 1 deletion docs/copyright.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ status, with these notes:
behind as unused files in `mu/resources/images`; the micro:bit plugin moved
them in (the Flash icon renamed from `flash`), byte-for-byte identical to
upstream Mu's, so they carry Mu's copyright and licence, nothing new.
* **The Python logo** (`python.png`, `python.svg`): see python.png above.
* **The Python logo** (`python.svg`): see python.png above.
* **Nu's own artwork.** The app icon (`icon.png`, and `package/icons` for the
installers) is Nu's, not Mu's. The splash screens (`splash_screen.png`,
`splash_fail.png`) and the Mode button (`modes.png`) are drawn from it by
`utils/make_artwork.py`, replacing Mu's logo artwork.

## Mu's design archive

Expand Down
3 changes: 3 additions & 0 deletions docs/modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ which may be of value for you to change are:
(CircuitPython sets it: a board restarts whenever a file on it is saved).
* `builtins` -- a list of strings defining symbols that Nu's code checker
must assume are builtins (above and beyond Python's standard builtins).
* `star_imports` -- a dict mapping a module to the names its `import *` brings
in (the micro:bit lists `from microbit import *`'s). The code checker reads
such an import as naming them, so it can still spot undefined names.
* `module_names` -- the module names the learner mustn't use for their own
files, because they'd hide a real module (CircuitPython lists the modules
built into CircuitPython).
Expand Down
1 change: 1 addition & 0 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ Two more things a mode can declare (new in 0.2):

| Member | Meaning |
|---|---|
| `star_imports = {}` | A dict mapping a module to the names its `import *` brings in (the micro:bit lists `from microbit import *`'s). **Check** reads such an import as naming them, so it can still spot undefined names. |
| `uses_user_environment = False` | Set it to `True` if the mode runs the learner's programs in Nu's Python environment (as Python 3 and Pygame Zero do). The admin dialog then shows that environment's variables and third-party packages. |
| `ensure_workspace_assets(workspace_dir)` | Called the first time the mode is used each time Nu runs (including the mode Nu starts in), once the workspace exists; a mode the learner doesn't use leaves the workspace alone. Put what the mode needs there (Pygame Zero makes its `images`, `fonts`, `sounds` and `music` folders and copies in demo files). Leave anything already there alone. Errors are reported like any other plugin error and don't stop Nu starting. |

Expand Down
16 changes: 8 additions & 8 deletions make.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

PYTEST = "pytest"
RUFF = "ruff"
# Paths ruff lints and formats. Excludes (mu/contrib, the API stubs, docs) are
# Paths ruff lints and formats. Excludes (the API stubs, docs) are
# configured in pyproject.toml's [tool.ruff].
RUFF_TARGETS = [
"make.py",
Expand All @@ -26,7 +26,6 @@
EXCLUDE_PATTERNS = {
"build/*",
"docs/*",
"mu/contrib/*",
"mu/modes/api/*",
"utils/*",
}
Expand Down Expand Up @@ -179,11 +178,9 @@ def clean():
_rmtree("site") # MkDocs build output
_rmtree(".pytest_cache")
_rmtree("lib")
_rmtree(".git/avatar/") # Created with `make video`
# TODO: recursive __pycache__ directories
_rmfiles(".", ".coverage")
_rmfiles(".", "*.egg-info")
_rmfiles(".", "*.mp4") # Created with `make video`
_rmfiles(".", "*.pyc")
_rmfiles("mu/locale", "*.pot")
_rmfiles("mu/wheels", "*.zip")
Expand Down Expand Up @@ -229,6 +226,10 @@ def translate_begin(lang=""):
mu_po_filename,
"--locale={locale}".format(locale=lang),
]
if update:
# Don't keep translations of strings Nu no longer has (#~ entries):
# git keeps the history, see utils/recover_renamed_translations.py.
cmd.append("--ignore-obsolete")
result = subprocess.run(cmd).returncode

print(
Expand All @@ -244,7 +245,6 @@ def translate_begin(lang=""):

_TRANSLATE_IGNORE_DIRNAMES = {
os.path.join("mu", "modes", "api"),
os.path.join("mu", "contrib"),
}
# Bundled plugins use Nu's own message catalog, so their packages are
# extracted too (but not their API stubs or tests).
Expand Down Expand Up @@ -345,7 +345,7 @@ def translate_status():

@export
def translate_test(lang=""):
"""Run translate_done and launch Mu in the given LANG."""
"""Run translate_done and launch Nu in the given LANG."""
result = translate_done(lang)
if result != 0:
raise RuntimeError("Failed compiling the mu.po file.")
Expand All @@ -357,10 +357,10 @@ def translate_test(lang=""):

@export
def run():
"""Run Mu from within a virtual environment"""
"""Run Nu from within a virtual environment"""
clean()
if not os.environ.get("VIRTUAL_ENV"):
raise RuntimeError("Cannot run Mu;your Python virtualenv is not activated")
raise RuntimeError("Cannot run Nu; your Python virtualenv is not activated")
return subprocess.run([sys.executable, "-m", "mu"]).returncode


Expand Down
Empty file removed mu/contrib/__init__.py
Empty file.
1 change: 0 additions & 1 deletion mu/interface/dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1358,7 +1358,6 @@ def setup(
settings,
packages,
mode,
device_list,
environment=None,
focus=None,
plugins=None,
Expand Down
27 changes: 0 additions & 27 deletions mu/interface/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,33 +435,6 @@ def show_annotations(self):
# aren't ints, so pass its value to keep the same style.
self.annotate(line, text, self.annotationDisplay().value)

def find_next_match(
self,
text,
from_line=-1,
from_col=-1,
case_sensitive=True,
wrap_around=True,
):
"""
Finds the next text match from the current cursor, or the given
position, and selects it (the automatic selection is the only available
QsciScintilla behaviour).
Returns True if match found, False otherwise.
"""
return self.findFirst(
text, # Text to find,
False, # Treat as regular expression
case_sensitive, # Case sensitive search
True, # Whole word matches only
wrap_around, # Wrap search
forward=True, # Forward search
line=from_line, # -1 starts at current position
index=from_col, # -1 starts at current position
show=False, # Unfolds found text
posix=False,
) # More POSIX compatible RegEx

def range_from_positions(self, start_position, end_position):
"""Given a start-end pair, such as are provided by a regex match,
return the corresponding Scintilla line-offset pairs which are
Expand Down
2 changes: 0 additions & 2 deletions mu/interface/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,6 @@ def show_admin(
settings,
packages,
mode,
device_list,
environment=None,
focus=None,
plugins=None,
Expand All @@ -970,7 +969,6 @@ def show_admin(
settings,
packages,
mode,
device_list,
environment=environment,
focus=focus,
plugins=plugins,
Expand Down
3 changes: 0 additions & 3 deletions mu/interface/panes.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,9 +697,6 @@ def stop_process(self):
self.process.waitForFinished()
self.running = False

def _del_(self):
self.stop_process()

def finished(self, code, status):
"""
Handle when the child process finishes.
Expand Down
Loading
Loading