diff --git a/plugins/kernel/pyproject.toml b/plugins/kernel/pyproject.toml index 1dd9f67..2650631 100644 --- a/plugins/kernel/pyproject.toml +++ b/plugins/kernel/pyproject.toml @@ -23,6 +23,7 @@ classifiers = [ ] dependencies = [ "txl", + "asphalt", "python-dateutil >=2.8.2", "pycrdt >=0.8.11,<0.9.0", ] diff --git a/plugins/kernel/txl_kernel/driver.py b/plugins/kernel/txl_kernel/driver.py index c25babf..840d9ef 100644 --- a/plugins/kernel/txl_kernel/driver.py +++ b/plugins/kernel/txl_kernel/driver.py @@ -2,6 +2,7 @@ import time from typing import Dict +from asphalt.core import Event, Signal from pycrdt import Array, Map from .message import create_message @@ -31,7 +32,15 @@ def send(self, buffers): asyncio.create_task(self.send_message(msg, self.shell_channel, change_date_to_str=True)) +class BusyEvent(Event): + def __init__(self, source, topic, busy: bool): + super().__init__(source, topic) + self.busy = busy + + class KernelMixin: + busy = Signal(BusyEvent) + def __init__(self): self.msg_cnt = 0 self.execute_requests: Dict[str, Dict[str, asyncio.Queue]] = {} @@ -89,6 +98,12 @@ async def recv(self): elif msg_type == "comm_msg": for comm_handler in self.comm_handlers: comm_handler.comm_msg(msg) + elif msg_type == "status": + execution_state = msg["content"]["execution_state"] + if execution_state == "idle": + self.busy.dispatch(False) + elif execution_state == "busy": + self.busy.dispatch(True) msg_id = msg["parent_header"].get("msg_id") if msg_id in self.execute_requests: # msg["header"] = str_to_date(msg["header"]) diff --git a/plugins/notebook_editor/txl_notebook_editor/components.py b/plugins/notebook_editor/txl_notebook_editor/components.py index 73f3bd7..ef71195 100644 --- a/plugins/notebook_editor/txl_notebook_editor/components.py +++ b/plugins/notebook_editor/txl_notebook_editor/components.py @@ -7,9 +7,12 @@ import anyio from asphalt.core import Component, Context from httpx import AsyncClient +from textual.app import RenderResult from textual.containers import VerticalScroll from textual.events import Event from textual.keys import Keys +from textual.reactive import Reactive +from textual.widget import Widget from textual.widgets import Select from txl.base import ( @@ -27,6 +30,33 @@ ydocs = {ep.name: ep.load() for ep in entry_points(group="jupyter_ydoc")} +class TopBar(Widget): + DEFAULT_CSS = """ + TopBar { + dock: top; + width: 100%; + background: $foreground 5%; + text-align: right; + color: $text; + height: 1; + } + """ + _busy_indicator = Reactive("○") + _busy = False + + @property + def busy(self) -> bool: + return self._busy + + @busy.setter + def busy(self, value: bool): + self._busy = value + self._busy_indicator = "◉" if value else "○" + + def render(self) -> RenderResult: + return self._busy_indicator + + class NotebookEditorMeta(type(Editor), type(VerticalScroll)): pass @@ -55,6 +85,11 @@ def __init__( self.edit_mode = False self.nb_change_target = asyncio.Queue() self.nb_change_events = asyncio.Queue() + self.top_bar = TopBar() + self.mount(self.top_bar) + + async def watch_busy(self, event): + self.top_bar.busy = event.busy async def on_open(self, event: FileOpenEvent) -> None: await self.open(event.path) @@ -123,6 +158,7 @@ def update(self): kernel_name = ipynb.get("metadata", {}).get("kernelspec", {}).get("name") if kernel_name: self.kernel = self.kernels(kernel_name) + self.kernel.kernel.busy.connect(self.watch_busy) for i_cell in range(self.ynb.cell_number): cell = self.cell_factory( self.ynb.ycells[i_cell], self.language, self.kernel @@ -152,6 +188,7 @@ async def observe_nb_changes(self): kernel_name = kernelspec.get("name") if kernel_name: self.kernel = self.kernels(kernel_name) + self.kernel.kernel.busy.connect(self.watch_busy) elif target == "state": if "dirty" in events.keys: dirty = events.keys["dirty"]["newValue"]