Skip to content

Commit 843e91d

Browse files
authored
Merge ability to bypass tmux for graphics
Added an option to select graphic protocol + tmux bypass.
2 parents eff76b1 + 80d71ba commit 843e91d

File tree

5 files changed

+48
-10
lines changed

5 files changed

+48
-10
lines changed

src/netbook/__init__.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import nbformat
99

1010
import textual
11+
import textual_image.widget
1112

1213
import traitlets
1314

@@ -19,8 +20,20 @@ class JupyterNetbook(jupyter_core.application.JupyterApp, jupyter_client.console
1920
description = """
2021
A terminal-based app for jupyter notebooks.
2122
"""
23+
graphics = traitlets.Enum(
24+
["auto", "kitty", "sixel", "halfcell", "unicode", "tmux-kitty"],
25+
default_value="auto",
26+
help="Choice of image rendering protocol",
27+
config=True,
28+
)
2229
flags = traitlets.Dict({**jupyter_core.application.base_flags, **jupyter_client.consoleapp.app_flags})
23-
aliases = traitlets.Dict({**jupyter_core.application.base_aliases, **jupyter_client.consoleapp.app_aliases})
30+
aliases = traitlets.Dict(
31+
{
32+
**jupyter_core.application.base_aliases,
33+
**jupyter_client.consoleapp.app_aliases,
34+
**{"graphics": "JupyterNetbook.graphics"},
35+
}
36+
)
2437

2538
kernel_client_class = jupyter_client.asynchronous.AsyncKernelClient
2639
# kernel_manager_class = jupyter_client.manager.AsyncKernelManager
@@ -42,6 +55,26 @@ def initialize(self, argv):
4255
if self._dispatching:
4356
return
4457

58+
match self.graphics:
59+
case "auto":
60+
image_class = textual_image.widget.AutoImage
61+
case "kitty":
62+
image_class = textual_image.widget.TGPImage
63+
case "sixel":
64+
image_class = textual_image.widget.SixelImage
65+
case "halfcell":
66+
image_class = textual_image.widget.HalfcellImage
67+
case "unicode":
68+
image_class = textual_image.widget.UnicodeImage
69+
case "tmux-kitty":
70+
import textual_image.renderable as r
71+
72+
r.tgp._TGP_MESSAGE_START = "\x1bPtmux;\x1b\x1b_G"
73+
r.tgp._TGP_MESSAGE_END = "\x1b\x1b\\\x1b\\"
74+
image_class = textual_image.widget.TGPImage
75+
case _:
76+
assert False, "unrichable"
77+
4578
nb = None
4679
nbkernel = None
4780
# Determine the notebook name and content
@@ -59,16 +92,16 @@ def initialize(self, argv):
5992
suffix = suffix + 1 if suffix else 1
6093
nbfile = f"Untitled{suffix}.ipynb"
6194

62-
if nbkernel and (
63-
"JupyterNetbook" not in self.cli_config or "kernel_name" not in self.cli_config["JupyterNetbook"]
64-
):
95+
if nbkernel and "kernel_name" not in self.cli_config.get("JupyterNetbook", {}):
6596
# If kernel_name is not explicitly specify but is present in the notebook, use that one
6697
self.kernel_name = nbkernel
6798

6899
jupyter_client.consoleapp.JupyterConsoleApp.initialize(self, argv)
69100

70101
# TODO: if --existing is specified, then self.kernel_manager is None.
71-
self.textual_app = JupyterTextualApp(self.kernel_manager, self.kernel_client, nbfile, nb)
102+
self.textual_app = JupyterTextualApp(
103+
self.kernel_manager, self.kernel_client, nbfile, nb, image_class=image_class
104+
)
72105

73106
def start(self):
74107
jupyter_core.application.JupyterApp.start(self)

src/netbook/_cell.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ class DisplayData(Output):
104104
def __init__(self, data: dict[str, tp.Any], metadata: dict[str, tp.Any]) -> None:
105105
super().__init__()
106106
self.data, self.metadata = data, metadata
107+
self.app: JupyterTextualApp
107108

108109
@tp.override
109110
def to_nbformat(self) -> nbformat.NotebookNode:
@@ -124,7 +125,7 @@ def compose(self) -> tp.Iterable[textual.widgets.Widget]:
124125
if image_key == "image/svg+xml"
125126
else base64.b64decode(self.data[image_key])
126127
)
127-
image = textual_image.widget.Image(io.BytesIO(image_bytes))
128+
image = self.app.image_class(io.BytesIO(image_bytes))
128129
# We'll set the width/height explicitly since automatically setting it seems hard / impossible.
129130
cell_size = textual_image.widget.get_cell_size()
130131
image.styles.width = round(image._image_width / cell_size.width)

src/netbook/_textual_app.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def __init__(
7070
kernel_client: jupyter_client.AsyncKernelClient,
7171
nbfile: str,
7272
nb: nbformat.NotebookNode | None,
73+
image_class: type,
7374
) -> None:
7475
super().__init__()
7576
self.kernel_manager = kernel_manager
@@ -100,6 +101,8 @@ def __init__(
100101
self.repeat_key_count = 0
101102
self.last_key_press_time = time.monotonic()
102103

104+
self.image_class = image_class
105+
103106
self._load_language()
104107
self.call_after_refresh(lambda: self.queue_for_kernel(self._initialize_kernel))
105108

tests/conftest.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
with unittest.mock.patch("sys.__stdout__", None):
66
import textual_image.renderable
77
import textual_image._terminal
8+
import textual_image.widget
89

910
# Hardcode the cell size - it throws on windows.
1011
setattr(textual_image._terminal.get_cell_size, "_result", textual_image._terminal.CellSize(10, 20))
@@ -22,7 +23,7 @@ async def pilot(mocker):
2223
km.kernel_spec.language = "python"
2324
kc = mocker.Mock()
2425
kc.execute_interactive = mocker.AsyncMock()
25-
app = netbook.JupyterTextualApp(km, kc, "", nb)
26+
app = netbook.JupyterTextualApp(km, kc, "", nb, image_class=textual_image.widget.HalfcellImage)
2627
async with app.run_test() as pilot:
2728
await pilot.pause()
2829
yield pilot
@@ -38,7 +39,7 @@ async def pilot_nb(mocker):
3839
km.kernel_spec.display_name = nb.metadata.kernelspec.display_name
3940
kc = mocker.Mock()
4041
kc.execute_interactive = mocker.AsyncMock()
41-
app = netbook.JupyterTextualApp(km, kc, nbfile, nb)
42+
app = netbook.JupyterTextualApp(km, kc, nbfile, nb, image_class=textual_image.widget.HalfcellImage)
4243
async with app.run_test() as pilot:
4344
await pilot.pause()
4445
yield pilot

tests/test_app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ async def test_load_language(mocker: pytest_mock.MockerFixture):
218218
km.kernel_spec.language = "julia"
219219
kc = mocker.Mock()
220220
kc.execute_interactive = mocker.AsyncMock()
221-
app = netbook.JupyterTextualApp(km, kc, "", nb)
221+
app = netbook.JupyterTextualApp(km, kc, "", nb, image_class=None)
222222
assert app.tree_sitter_language is not None
223223
assert app.language_highlights_query != ""
224224
async with app.run_test() as pilot:
@@ -228,7 +228,7 @@ async def test_load_language(mocker: pytest_mock.MockerFixture):
228228
# Test when we can't load the language
229229
km.kernel_spec.language = "R"
230230
mocker.patch("netbook._textual_app.JupyterTextualApp.notify")
231-
app = netbook.JupyterTextualApp(km, kc, "", nb)
231+
app = netbook.JupyterTextualApp(km, kc, "", nb, image_class=None)
232232
app.notify.assert_called_once_with(
233233
"Syntax highlighting is not available for R. Try installing the package `tree_sitter_r`"
234234
)

0 commit comments

Comments
 (0)