Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ jobs:

- name: Run jpterm
run: uv run jpterm --help

- name: Run tests
run: uv run --group test pytest tests -v --color=yes
41 changes: 10 additions & 31 deletions docs/usage/CLI.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,19 @@
Once jpterm has been installed, it can be launched from the command line. You can show the help message like so:
When jpterm is invoked with a command, it runs as a CLI in your terminal without taking control of it, inlining potential outputs below the command.

```bash
$ jpterm --help

Usage: jpterm [OPTIONS]

╭─ Options ─────────────────────────────────────────────────────────────────────╮
│ --logo Show the jpterm logo. │
│ --server TEXT The URL to the Jupyter server. │
│ --collaborative/--no-collaborative Collaborative mode (with a server). │
│ --experimental/--no-experimental Experimental mode (with Jupyverse). │
│ --configfile TEXT Read YAML configuration file. │
│ --set TEXT Set configuration. │
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────────────────╯
```

The easiest way to launch jpterm is simply by entering `jpterm` and hitting *Enter*. Jpterm will run locally on your machine, without needing a Jupyter server.

Jpterm can also be a client to a Jupyter server, just like when you use JupyterLab. While JupyterLab is a Web client that runs in the browser, jpterm runs in the terminal. The two main Jupyter servers are [jupyter-server](https://github.com/jupyter-server/jupyter_server) and [jupyverse](https://github.com/jupyter-server/jupyverse). You can install and launch them like so:
Use the `show` command to display the (rich) content of a file:

```bash
# Install jupyter-server:
pip install "jupyterlab>=4"
pip install jupyter-collaboration
# Launch it:
jupyter lab --ServerApp.token='' --ServerApp.password='' --ServerApp.disable_check_xsrf=True --no-browser --port=8000
# Without a server:
jpterm show path/to/file

# Or install jupyverse:
pip install "jupyverse[jupyterlab, noauth]"
# Launch it:
jupyverse
# With a server:
jpterm --server http://127.0.0.1:8000 show path/to/file
```

Then pass the URL of the Jupyter server to jpterm through the `--server` option:
By "rich content" we mean that for instance, a Python file will be rendered with Python syntax highlighting, a Jupyter notebook will be rendered with a nice view of cells, etc.

If you pass `--json` then the content will not be represented as a rich output but as JSON. This can be useful for passing the output to another tool:

```bash
jpterm --server http://127.0.0.1:8000
jpterm show path/to/notebook --json | jq -r '.cells[0].source'
```
43 changes: 43 additions & 0 deletions docs/usage/TUI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
Once jpterm has been installed, it can be launched from the command line. You can show the help message like so:

```
$ jpterm --help

Usage: jpterm [OPTIONS] [COMMAND] [ARGS]...

╭─ Options ──────────────────────────────────────────────────────────────────────────────────────╮
│ --logo Show the jpterm logo. │
│ --server TEXT The URL to the Jupyter server. │
│ --backend TEXT The name of the event loop to use (asyncio or trio). │
│ --collaborative/--no-collaborative Collaborative mode (with a server). │
│ --experimental/--no-experimental Experimental mode (with Jupyverse). │
│ --set TEXT Set configuration. │
│ --help Show this message and exit. │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ─────────────────────────────────────────────────────────────────────────────────────╮
│ show Show document PATH inline using its registered editor. │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
```

The easiest way to launch jpterm is simply by entering `jpterm` and hitting *Enter*. Jpterm will run locally on your machine, without needing a Jupyter server. This is the TUI mode (Text User Interface), where jpterm takes control of your terminal and runs as a full-screen application. There is also a [CLI](CLI.md) mode (Command Line Interface).

Jpterm can also run as a client to a Jupyter server, just like when you use JupyterLab. While JupyterLab is a Web client that runs in the browser, jpterm runs in the terminal. The two main Jupyter servers are [jupyter-server](https://github.com/jupyter-server/jupyter_server) and [jupyverse](https://github.com/jupyter-server/jupyverse). You can install and launch them like so:

```bash
# Install jupyter-server (through JupyterLab):
pip install jupyterlab
pip install jupyter-collaboration
# Launch it:
jupyter lab --ServerApp.token='' --ServerApp.password='' --ServerApp.disable_check_xsrf=True --no-browser --port=8000

# Or install jupyverse:
pip install "jupyverse[jupyterlab,noauth,collaboration]"
# Launch it:
jupyverse
```

Then pass the URL of the Jupyter server to jpterm through the `--server` option:

```bash
jpterm --server http://127.0.0.1:8000
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ nav:
- Overview: index.md
- install.md
- Usage:
- usage/TUI.md
- usage/CLI.md
- usage/key_bindings.md
- Plugins:
Expand Down
9 changes: 6 additions & 3 deletions plugins/editors/txl_editors/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def register_editor_factory(
self.ext_editor_factories[ext] = []
self.ext_editor_factories[ext].append(editor_factory)

async def on_open(self, path: str) -> None:
def create_editor(self, path: str) -> Editor:
path = Path(path)
extension = path.suffix
for ext, editor_factories in self.ext_editor_factories.items():
Expand All @@ -47,8 +47,11 @@ async def on_open(self, path: str) -> None:
if None not in self.ext_editor_factories:
raise RuntimeError(f"Could not find an editor for file extension {extension}")
preferred_editor_factory = self.ext_editor_factories[None][0]
preferred_editor = preferred_editor_factory()
self.main_area.show(preferred_editor, path.name)
return preferred_editor_factory()

async def on_open(self, path: str) -> None:
preferred_editor = self.create_editor(path)
self.main_area.show(preferred_editor, Path(path).name)
bindings = preferred_editor.get_bindings()
if bindings:
self.footer.update_bindings(bindings)
Expand Down
21 changes: 16 additions & 5 deletions plugins/jpterm/txl_jpterm/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from anyio import Event
from fps import Module
from textual._context import active_app
from textual.app import App
Expand All @@ -19,20 +20,30 @@ class Jpterm(App):
("q", "quit", "Quit"),
]
show_browser = var(True)
cli: bool = False

def __init__(self, header, footer, main_area, *args, **kwargs):
self.header = header
self.footer = footer
self.main_area = main_area
self.shell_ready = Event()
super().__init__(*args, **kwargs)

def start(self, launcher, file_browser) -> None:
self.launcher = launcher
self.file_browser = file_browser
self.shell_ready.set()

async def on_mount(self) -> None:
if self.cli:
return
await self.shell_ready.wait()
active_app.set(self)
self.mount(self.header)
container = Container(file_browser, self.main_area)
self.mount(container)
self.mount(self.footer)
self.main_area.show(launcher, "Launcher", mount=False)
await self.mount(self.header)
container = Container(self.file_browser, self.main_area)
await self.mount(container)
await self.mount(self.footer)
self.main_area.show(self.launcher, "Launcher", mount=False)

def watch_show_browser(self, show_browser: bool) -> None:
self.set_class(show_browser, "-show-browser")
Expand Down
17 changes: 12 additions & 5 deletions plugins/markdown_viewer/txl_markdown_viewer/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,24 @@ async def open(self, path: str) -> None:
self.ytext = await self.contents.get(path, type="file")
self.focus()
await self._update_viewer()
self.ytext.observe(self.on_change)
if not self.app.cli:
self.ytext.observe(self.on_change)

async def _update_viewer(self):
md = self.ytext.source
if self.viewer is not None:
self.viewer.remove()
self.viewer = TextualMarkdownViewer(md, show_table_of_contents=True)
self.mount(self.viewer)
await self.viewer.remove()
self.viewer = TextualMarkdownViewer(show_table_of_contents=not self.app.cli)
if self.app.cli:
self.viewer.styles.height = "auto"
self.viewer.styles.overflow_y = "hidden"
self.viewer.styles.scrollbar_size_vertical = 0
self.viewer.styles.scrollbar_gutter = "auto"
await self.mount(self.viewer)
await self.viewer.document.update(md)

def on_change(self, target, event):
self.task_group.create_task(self.update_viewer())
self.task_group.create_task(self._update_viewer())


class MarkdownViewerModule(Module):
Expand Down
2 changes: 1 addition & 1 deletion plugins/notebook_editor/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ requires-python = ">=3.10"
license = "MIT"
keywords = []
classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy",]
dependencies = [ "txl ==0.3.3", "pycrdt >=0.14.1,<0.15.0", "jupyter-ydoc >=4.1.1,<5.0.0",]
dependencies = [ "httpx2>=2.6.0", "txl ==0.3.3", "pycrdt >=0.14.1,<0.15.0", "jupyter-ydoc >=4.1.1,<5.0.0",]
[[project.authors]]
name = "David Brochart"
email = "david.brochart@gmail.com"
Expand Down
36 changes: 23 additions & 13 deletions plugins/notebook_editor/txl_notebook_editor/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import anyio
from fps import Module
from httpx import AsyncClient
from httpx2 import AsyncClient
from textual._context import active_app
from textual.app import App, ComposeResult, RenderResult
from textual.containers import VerticalScroll
Expand Down Expand Up @@ -85,16 +85,11 @@ def __init__(
self.cell_i = 0
self.cell_copy = None
self.edit_mode = False
self.nb_change_target = StapledObjectStream(
*anyio.create_memory_object_stream[Any](max_buffer_size=math.inf)
)
self.nb_change_events = StapledObjectStream(
*anyio.create_memory_object_stream[Any](max_buffer_size=math.inf)
)
self.top_bar = TopBar()

def compose(self) -> ComposeResult:
yield self.top_bar
if not self.app.cli:
yield self.top_bar

def watch_busy(self, busy):
self.top_bar.busy = busy
Expand Down Expand Up @@ -153,13 +148,20 @@ async def open(self, path: str | None = None) -> None:
self.path = path
self.ynb = await self.contents.get(self.path, type="notebook", format="json")
self.update()
self.ynb.observe(self.on_change)
self.task_group.create_task(self.observe_nb_changes())
if not self.app.cli:
self.nb_change_target = StapledObjectStream(
*anyio.create_memory_object_stream[Any](max_buffer_size=math.inf)
)
self.nb_change_events = StapledObjectStream(
*anyio.create_memory_object_stream[Any](max_buffer_size=math.inf)
)
self.ynb.observe(self.on_change)
self.task_group.create_task(self.observe_nb_changes())

def update(self):
ipynb = self.ynb.source
self.language = ipynb.get("metadata", {}).get("kernelspec", {}).get("language", None)
if self.kernel is None:
if self.kernel is None and not self.app.cli:
kernel_name = ipynb.get("metadata", {}).get("kernelspec", {}).get("name")
if kernel_name:
self.kernel = self.kernels(kernel_name)
Expand All @@ -168,9 +170,15 @@ def update(self):
cell = self.cell_factory(
self.ynb.ycells[i_cell], self.language, self.kernel
)
if self.app.cli:
cell.source.read_only = True
cell.source.highlight_cursor_line = False
cell.source.show_cursor = False
cell.source.styles.max_height = None
cell.source.styles.scrollbar_size_vertical = 0
self.mount(cell)
self.cells.append(cell)
if self.cells:
if self.cells and not self.app.cli:
self.cells[self.cell_i].select()

def on_change(self, target, events):
Expand Down Expand Up @@ -240,6 +248,8 @@ async def observe_nb_changes(self):
idx += 1

async def on_key(self, event: Event) -> None:
if self.app.cli:
return
if event.key == Keys.Escape:
self.edit_mode = False
self.current_cell.focus()
Expand Down Expand Up @@ -416,7 +426,7 @@ async def start(self) -> None:
main_area = await self.get(MainArea)
app = await self.get(App)

_kernelspecs = await kernelspecs.get()
_kernelspecs = {} if app.cli else await kernelspecs.get()

async with anyio.create_task_group() as self.tg:
def notebook_editor_factory():
Expand Down
2 changes: 1 addition & 1 deletion plugins/remote_contents/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ requires-python = ">=3.10"
license = "MIT"
keywords = []
classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy",]
dependencies = [ "txl ==0.3.3", "httpx>=0.23.1", "httpx-ws>=0.4.2", "pycrdt >=0.14.1,<0.15.0", "pycrdt-websocket >=0.16.4,<0.17.0", "jupyter-ydoc >=4.1.1,<5.0.0",]
dependencies = [ "txl ==0.3.3", "httpx2[ws]>=2.6.0", "pycrdt >=0.14.1,<0.15.0", "pycrdt-websocket >=0.16.4,<0.17.0", "jupyter-ydoc >=4.1.1,<5.0.0",]
[[project.authors]]
name = "David Brochart"
email = "david.brochart@gmail.com"
Expand Down
Loading
Loading