Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,22 @@ For an API reference and usage examples, [see our online documentation](https://

A minimal configuration:

```bash
pip install catppuccin[pygments]
```

```python
c.InteractiveShellApp.extensions = ["catppuccin.extras.ipython"]
c.TerminalInteractiveShell.true_color = True
c.TerminalInteractiveShell.highlighting_style = "catppuccin-mocha"
c.Catppuccin.flavor = "mocha" # possible values: "latte", "frappe", "macchiato", "mocha"
```

Putting this into your [IPython configuration](https://ipython.readthedocs.io/en/stable/config/intro.html)
and ensuring `catppuccin[pygments]` is installed in the same environment will
give you Catppuccin Mocha syntax highlighting in the REPL. See [here](https://github.com/backwardspy/dots/blob/f6991570d6691212e27e266517656192f910ccbf/dot_config/ipython/profile_default/ipython_config.py)
for an example of a more complete configuration.

Available IPython themes:
give you Catppuccin Mocha syntax highlighting in the REPL.

- `catppuccin-latte`
- `catppuccin-frappe`
- `catppuccin-macchiato`
- `catppuccin-mocha`
> [!NOTE]
> The `Catppuccin` section in the IPython config file is custom and not part of the official IPython configuration. It is used by the Catppuccin extension to determine which flavor to apply. `TerminalInteractiveShell.colors` is not used because it is validated before the extension is loaded and as a result, the `theme_table` is not yet populated with Catppuccin themes.

## Contribution

Expand Down
1 change: 1 addition & 0 deletions catppuccin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
- [matplotlib](./catppuccin/extras/matplotlib.html)
- [pygments](./catppuccin/extras/pygments.html)
- [rich](./catppuccin/extras/rich_ctp.html)
- [IPython](./catppuccin/extras/ipython.html)

"""

Expand Down
72 changes: 72 additions & 0 deletions catppuccin/extras/ipython.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""IPython extension for Catppuccin themes.

This extension registers Catppuccin themes in IPython's internal
`PyColorize.theme_table` so that they can be used with `%colors` magic command and
configured via custom `Catppuccin` section in IPython's config file.

You can use this extension by adding the following to your IPython config file:

```python
c = get_config()
c.InteractiveShellApp.extensions = ["catppuccin.extras.ipython"]
c.TerminalInteractiveShell.true_color = True
# Optional: Set the flavor to use (default is "mocha")
# possible values: "latte", "frappe", "macchiato", "mocha"
c.Catppuccin.flavor = "mocha"
```

The reason for using a custom `Catppuccin` section instead of
`TerminalInteractiveShell.colors` is that the latter is validated before
the extension is loaded, which means that the `theme_table`
is not yet populated with Catppuccin themes.
"""

from __future__ import annotations

import warnings
from copy import deepcopy
from typing import TYPE_CHECKING

from catppuccin import PALETTE

if TYPE_CHECKING:
from IPython.core.interactiveshell import InteractiveShell


def register_themes() -> None:
"""Register Catppuccin flavors into IPython's `PyColorize.theme_table`."""
try:
from IPython.utils.PyColorize import linux_theme, theme_table
except ImportError:
return

for flavor in PALETTE:
theme_name = f"catppuccin-{flavor.identifier}"

try:
theme = deepcopy(linux_theme)
theme.base = theme_name
theme_table[theme_name] = theme
except Exception as e: # noqa: BLE001
warnings.warn(
f"Failed to register IPython theme '{theme_name}': {e}",
RuntimeWarning,
stacklevel=2,
)


def load_ipython_extension(ipython: InteractiveShell) -> None:
"""Load the Catppuccin IPython extension.

This function registers Catppuccin themes and sets the IPython color scheme
based on the custom `Catppuccin` section in the IPython config file.
"""
register_themes()

config = getattr(ipython, "config", {})
# Read from 'Catppuccin.flavor' because 'TerminalInteractiveShell.colors'
# is validated before this extension loads and populates 'theme_table'.
catppuccin_config = config.get("Catppuccin", {})
flavor = catppuccin_config.get("flavor", "mocha").strip().lower()
if flavor and flavor in (f.identifier for f in PALETTE):
ipython.run_line_magic("colors", f"catppuccin-{flavor}")
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ catppuccin-mocha = "catppuccin.extras.pygments:MochaStyle"
matplotlib = ["matplotlib>=3.8.4"]
pygments = ["pygments>=2.17.2"]
rich = ["rich>=13.7.0"]
ipython = ["ipython>=8.0.0"]
gh-pages = ["tinycss2>=1.2.1", "pdoc>=15.0.1"]

[dependency-groups]
Expand All @@ -31,6 +32,7 @@ dev = [
"ruff>=0.11.2",
"types-pygments>=2.19.0.20250305",
"types-colorama>=0.4.15.20240205",
"ipython>=8.0.0",
]

[tool.ruff.lint]
Expand Down
54 changes: 54 additions & 0 deletions tests/test_ipython.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import sys
from unittest.mock import MagicMock

import pytest
from IPython.utils.PyColorize import theme_table

from catppuccin import PALETTE
from catppuccin.extras.ipython import load_ipython_extension, register_themes

pytest.importorskip("IPython")


def test_ipython_themes_registered() -> None:
"""Test that Catppuccin themes are registered in IPython's theme_table."""
register_themes()

for flavor in PALETTE:
theme_name = f"catppuccin-{flavor.identifier}"
assert theme_name in theme_table
theme = theme_table[theme_name]
assert theme.base == theme_name


def test_load_ipython_extension_runs_magic() -> None:
"""Test that extension runs the %colors magic with the correct flavor."""
mock_shell = MagicMock()
mock_shell.config = {
"Catppuccin": {
"flavor": "mocha",
},
}

load_ipython_extension(mock_shell)

assert "catppuccin-mocha" in theme_table
mock_shell.run_line_magic.assert_called_once_with("colors", "catppuccin-mocha")


def test_register_themes_without_ipython(monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that register_themes exits gracefully when IPython is not available."""
monkeypatch.setitem(sys.modules, "IPython.utils.PyColorize", None)
register_themes()


def test_register_themes_handles_exception(monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that register_themes handles exceptions during theme registration."""

def mock_deepcopy(_: object) -> None:
raise RuntimeError

monkeypatch.setattr("catppuccin.extras.ipython.deepcopy", mock_deepcopy)

with pytest.warns(RuntimeWarning, match="Failed to register IPython theme"):
register_themes()
Loading