Skip to content

Commit 3c5097a

Browse files
committed
feat: new layout rendering is optimised for readability on tiny screen
1 parent e629b6a commit 3c5097a

File tree

6 files changed

+49
-65
lines changed

6 files changed

+49
-65
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "nbcat"
3-
version = "0.10.0"
3+
version = "0.11.0"
44
description = "cat for jupyter notebooks"
55
authors = [
66
{ name = "Akop Kesheshyan", email = "devnull@akop.dev" }

src/nbcat/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.10.0"
1+
__version__ = "0.11.0"

src/nbcat/image.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,22 @@
1010

1111

1212
class Image:
13-
def __init__(self, image: str):
13+
def __init__(self, image: str, method: str = "a24h"):
1414
img = BytesIO(base64.b64decode(image.replace("\n", "")))
1515
self.image = PilImage.open(img)
16-
17-
@property
18-
def method_class(self):
19-
# TODO: auto detect terminal to benefit from sixel protocol support
20-
method = "a24h" if system() != "Windows" else "ascii"
21-
return METHODS[method]["class"]
16+
self.method = method if system() != "Windows" else "ascii"
2217

2318
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
24-
img = Renderer()
25-
img.load_image(self.image)
26-
img.resize(shutil.get_terminal_size()[0] - 1)
27-
output = img.to_string(self.method_class)
28-
yield Text.from_ansi(output)
19+
renderer = Renderer()
20+
renderer.load_image(self.image)
21+
width = shutil.get_terminal_size()[0] - 1
22+
if self.method == "sixel":
23+
width = width * 6
24+
25+
renderer.resize(width)
26+
27+
if self.method == "sixel":
28+
renderer.reduce_colors(16)
29+
30+
output = renderer.to_string(METHODS[self.method]["class"])
31+
yield Text.from_ansi(output, no_wrap=True, end="")

src/nbcat/main.py

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
import argparse
22
import sys
33
from pathlib import Path
4-
from typing import Union
54

65
import argcomplete
76
import requests
87
from argcomplete.completers import FilesCompleter
98
from pydantic import ValidationError
109
from rich import box
11-
from rich.console import Console, RenderableType
10+
from rich.console import Console, Group, RenderableType
11+
from rich.padding import Padding
1212
from rich.panel import Panel
1313
from rich.pretty import Pretty
1414
from rich.syntax import Syntax
15-
from rich.table import Table
1615
from rich.text import Text
1716

1817
from . import __version__
@@ -69,7 +68,7 @@ def read_notebook(fp: str, debug: bool = False) -> Notebook:
6968
raise InvalidNotebookFormatError(f"Invalid notebook: {e}")
7069

7170

72-
def render_cell(cell: Cell) -> list[tuple[Union[str, None], RenderableType]]:
71+
def render_cell(cell: Cell) -> RenderableType:
7372
"""
7473
Render the content of a notebook cell for display.
7574
@@ -86,15 +85,17 @@ def render_cell(cell: Cell) -> list[tuple[Union[str, None], RenderableType]]:
8685
"""
8786

8887
def _render_markdown(input: str) -> Markdown:
89-
return Markdown(input)
88+
return Markdown(input, code_theme="ansi_dark")
9089

91-
def _render_code(input: str) -> Panel:
92-
return Panel(Syntax(input, "python", theme="ansi_dark"), box=box.SQUARE)
90+
def _render_code(input: str, language: str = "python") -> Panel:
91+
return Panel(
92+
Syntax(input, language, line_numbers=True, theme="ansi_dark", dedent=True), padding=0
93+
)
9394

9495
def _render_raw(input: str) -> Text:
9596
return Text(input)
9697

97-
def _render_image(input: str) -> None:
98+
def _render_image(input: str) -> Image:
9899
return Image(input)
99100

100101
def _render_json(input: str) -> Pretty:
@@ -111,31 +112,21 @@ def _render_json(input: str) -> Pretty:
111112
OutputCellType.JSON: _render_json,
112113
}
113114

114-
rows: list[tuple[Union[str, None], RenderableType]] = []
115+
rows: list[RenderableType] = []
115116
renderer = RENDERERS.get(cell.cell_type)
116117
source = renderer(cell.input) if renderer else None
117118
if source:
118-
label = f"[green][{cell.execution_count}][/]" if cell.execution_count else None
119-
rows.append(
120-
(
121-
label,
122-
source,
123-
)
124-
)
119+
rows.append(Padding(source, (1, 0)))
120+
if not cell.outputs:
121+
return source
125122

126123
for o in cell.outputs:
127124
if o.output:
128125
renderer = RENDERERS.get(o.output.output_type)
129126
output = renderer(o.output.text) if renderer else None
130127
if output:
131-
label = f"[blue][{o.execution_count}][/]" if o.execution_count else None
132-
rows.append(
133-
(
134-
label,
135-
output,
136-
)
137-
)
138-
return rows
128+
rows.append(Panel(output, style="italic", box=box.MINIMAL))
129+
return Group(*rows)
139130

140131

141132
def print_notebook(nb: Notebook):
@@ -149,15 +140,17 @@ def print_notebook(nb: Notebook):
149140
console.print("[bold red]Notebook contains no cells.")
150141
return
151142

152-
layout = Table.grid(padding=1)
153-
layout.add_column(no_wrap=True, width=6)
154-
layout.add_column()
155-
156143
for cell in nb.cells:
157-
for label, content in render_cell(cell):
158-
layout.add_row(label, content)
159-
160-
console.print(layout)
144+
rendered = render_cell(cell)
145+
if isinstance(rendered, Group):
146+
out = Panel(
147+
rendered,
148+
title=f"[green][{cell.execution_count}][/]" if cell.execution_count else None,
149+
title_align="left",
150+
)
151+
else:
152+
out = Padding(rendered, (1, 0))
153+
console.print(out)
161154

162155

163156
def main():

tests/test_render_cell.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytest
2+
from rich.console import Group, RenderableType
23
from rich.markdown import Markdown
34
from rich.panel import Panel
45
from rich.text import Text
@@ -16,14 +17,11 @@
1617
("heading", "Heading text", Markdown),
1718
],
1819
)
19-
def test_render_cell_input_rendering(cell_type: str, source: str, expected):
20+
def test_render_cell_input_rendering(cell_type: str, source: str, expected: RenderableType):
2021
cell = Cell(cell_type=cell_type, source=source, execution_count=42, outputs=[])
2122
rendered = render_cell(cell)
2223

23-
assert len(rendered) == 1
24-
label, content = rendered[0]
25-
assert label == "[green][42][/]"
26-
assert isinstance(content, expected)
24+
assert isinstance(rendered, expected)
2725

2826

2927
def test_render_cell_with_outputs():
@@ -39,16 +37,8 @@ def test_render_cell_with_outputs():
3937

4038
rendered = render_cell(cell)
4139

42-
print(rendered)
43-
assert len(rendered) == 3
44-
assert rendered[0][0] is None
45-
assert isinstance(rendered[0][1], Panel)
46-
47-
assert rendered[1][0] == "[blue][7][/]"
48-
assert isinstance(rendered[1][1], Text)
49-
50-
assert rendered[2][0] is None
51-
assert isinstance(rendered[2][1], Text)
40+
assert isinstance(rendered, Group)
41+
assert len(rendered.renderables) == 3
5242

5343

5444
def test_render_cell_skips_empty_outputs():
@@ -62,6 +52,4 @@ def test_render_cell_skips_empty_outputs():
6252

6353
rendered = render_cell(cell)
6454

65-
assert len(rendered) == 1 # Only source input is rendered
66-
assert rendered[0][0] == "[green][1][/]"
67-
assert isinstance(rendered[0][1], Text)
55+
assert isinstance(rendered, Group)

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)