From f4eee2f78db8abfaeac1b958e5ef21134d945f10 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Mon, 11 Nov 2024 20:38:45 +1100 Subject: [PATCH] Shuffle code and rework sessions. --- examples/menu.ipynb | 4 +- examples/plugins.ipynb | 5 +- examples/sessions.ipynb | 54 ++++---------------- ipylab/commands.py | 14 +++--- ipylab/connection.py | 5 ++ ipylab/dialog.py | 3 +- ipylab/jupyterfrontend.py | 7 +-- ipylab/sessions.py | 18 +++++-- ipylab/shell.py | 30 +++++++---- src/widget.ts | 3 +- src/widgets/connection.ts | 49 ++++++------------ src/widgets/frontend.ts | 79 +++++++++++++---------------- src/widgets/ipylab.ts | 13 +++-- src/widgets/sessions.ts | 34 +++++++++++++ src/widgets/shell.ts | 101 ++++++++++++++++++++++++++++++++------ 15 files changed, 245 insertions(+), 174 deletions(-) create mode 100644 src/widgets/sessions.ts diff --git a/examples/menu.ipynb b/examples/menu.ipynb index c08382c..c5dc59e 100644 --- a/examples/menu.ipynb +++ b/examples/menu.ipynb @@ -204,8 +204,8 @@ "metadata": {}, "outputs": [], "source": [ - "async def show_id(active_widget: ipylab.ShellConnection):\n", - " id_ = await active_widget.get_property(\"id\")\n", + "async def show_id(current_widget: ipylab.ShellConnection):\n", + " id_ = await current_widget.get_property(\"id\")\n", " await app.dialog.show_dialog(\"Show id\", f\"Widget id is {id_}\")\n", "\n", "\n", diff --git a/examples/plugins.ipynb b/examples/plugins.ipynb index f04c8f6..9058dd4 100644 --- a/examples/plugins.ipynb +++ b/examples/plugins.ipynb @@ -117,10 +117,9 @@ "Then try.\n", "```python\n", "\n", - "test # Should print 'Test' as defined in the 'namespace_objects' plugin.\n", - "\n", "# Now lets load a different namespace is available.\n", "app.activate_namespace('test')\n", + "test # Should print 'Test' as defined in the 'namespace_objects' plugin.\n", "dir()\n", "\n", "# To switch back use\n", @@ -269,7 +268,7 @@ "\n", "#### Command\n", "\n", - "The ipylab kernel can be started/re-started with the command 'Start ipylab kernel' (`Ctrl c` -> 'Start ipylab kernel').\n", + "The ipylab kernel can be started/re-started with the command 'Start ipylab kernel' (`Ctrl c` -> 'Start or restart ipylab kernel').\n", "\n", "#### Configure\n", "\n", diff --git a/examples/sessions.ipynb b/examples/sessions.ipynb index 7ce263c..3558fa5 100644 --- a/examples/sessions.ipynb +++ b/examples/sessions.ipynb @@ -47,14 +47,7 @@ "metadata": {}, "outputs": [], "source": [ - "app.all_sessions" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Show current session" + "t = app.sessions.get_running()" ] }, { @@ -63,33 +56,14 @@ "metadata": {}, "outputs": [], "source": [ - "app.current_session" + "t.result()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Example: Create Console with current session\n", - "The following two commands should both create a console panel sharing the same `session` as the current notebook." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "app.commands.execute(\"console:create\", app.current_session)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Create a new Notebook\n", - "\n", - "Create a new notebook and use the connection to interact with the notebook directly." + "## Show current session" ] }, { @@ -98,7 +72,7 @@ "metadata": {}, "outputs": [], "source": [ - "t = app.commands.execute(\"notebook:create-new\", transform=ipylab.Transform.connection)" + "t = app.sessions.get_current()" ] }, { @@ -107,24 +81,16 @@ "metadata": {}, "outputs": [], "source": [ - "nb = t.result()\n", - "nb # A Connection to the notebook." + "session = t.result()\n", + "session" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We can use the connection to the notebook to do various actions. Lets list the attributes of the context." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "nb.execute_method(\"context.rename\", \"My renamed notebook.ipynb\")" + "## Example: Create Console with current session\n", + "This command creates a new console sharing the same `session` as the current notebook." ] }, { @@ -133,7 +99,7 @@ "metadata": {}, "outputs": [], "source": [ - "nb.close()" + "app.commands.execute(\"console:create\", session)" ] } ], @@ -153,7 +119,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.11.10" } }, "nbformat": 4, diff --git a/ipylab/commands.py b/ipylab/commands.py index c5cb722..c788fa9 100644 --- a/ipylab/commands.py +++ b/ipylab/commands.py @@ -116,14 +116,14 @@ def add( Special args ------------ - * active_widget: ShellConnection + * current_widget: ShellConnection * ref: ShellConnection Include in the argument list of the function to have the value provided when the command is called. - active_widget: - This is a ShellConnection to the Jupyterlab defined active widget. + current_widget: + This is a ShellConnection to the Jupyterlab defined current widget. For the command to appear in the context menu non-ipylab widgets, the appropriate selector should be used. see: https://jupyterlab.readthedocs.io/en/stable/developer/css.html#commonly-used-css-selectors Selectors: @@ -131,10 +131,10 @@ def add( * Main area: '.jp-Activity' ref: - This is a ShellConnection to the Ipylab active widget. + This is a ShellConnection to the Ipylab current widget. The associated widget/panel is then accessible by `ref.widget`. - Tip: This is can be used in context menus to perform actions specific to the active widget + Tip: This is can be used in context menus to perform actions specific to the current widget in the shell. """ cid = CommandPalletItemConnection.to_cid(command, category) @@ -198,12 +198,12 @@ async def _execute_for_frontend(self, payload: dict, buffers: list): args = conn.args | (payload.get("args") or {}) | {"buffers": buffers} # Shell connections - cids = {"active_widget": payload["cid1"], "ref": payload["cid2"]} + cids = {"current_widget": payload["cid1"], "ref": payload["cid2"]} glbls = ipylab.app.get_namespace(conn.namespace_name) kwgs = {} for n, p in inspect.signature(cmd).parameters.items(): - if n in ["active_widget", "ref"] and cids[n]: + if n in ["current_widget", "ref"] and cids[n]: kwgs[n] = ShellConnection(cids[n]) await kwgs[n].ready() elif n in args: diff --git a/ipylab/connection.py b/ipylab/connection.py index c256a4a..81e8efe 100644 --- a/ipylab/connection.py +++ b/ipylab/connection.py @@ -14,6 +14,7 @@ from ipylab.ipylab import Ipylab if TYPE_CHECKING: + from asyncio import Task from collections.abc import Generator from typing import Literal, Self, overload @@ -169,3 +170,7 @@ def activate(self): "Activate the connected widget in the shell." return self.operation("activate") + + def get_session(self) -> Task[dict]: + """Get the session of the connected widget.""" + return self.operation("getSession") diff --git a/ipylab/dialog.py b/ipylab/dialog.py index ee529bd..99db1bd 100644 --- a/ipylab/dialog.py +++ b/ipylab/dialog.py @@ -117,7 +117,8 @@ def show_dialog( see: https://jupyterlab.readthedocs.io/en/stable/api/functions/apputils.showDialog.html source: https://jupyterlab.readthedocs.io/en/stable/extension/ui_helpers.html#generic-dialog """ - kwgs["toLuminoWidget"] = ["body"] if isinstance(body, Widget) else [] + if isinstance(body, Widget) and "toLuminoWidget" not in kwgs: + kwgs["toLuminoWidget"] = ["body"] return self.operation("showDialog", _combine(options, title=title, body=body), **kwgs) def show_error_message( diff --git a/ipylab/jupyterfrontend.py b/ipylab/jupyterfrontend.py index 1740e58..c1113ac 100644 --- a/ipylab/jupyterfrontend.py +++ b/ipylab/jupyterfrontend.py @@ -53,10 +53,7 @@ class App(Ipylab): _model_name = Unicode("JupyterFrontEndModel").tag(sync=True) ipylab_base = IpylabBase(Obj.IpylabModel, "app").tag(sync=True) version = Unicode(read_only=True).tag(sync=True) - current_widget_id = Unicode(read_only=True).tag(sync=True) logger_level = UseEnum(LogLevel, read_only=True, default_value=LogLevel.warning).tag(sync=True) - current_session = Dict(read_only=True).tag(sync=True) - all_sessions = Tuple(read_only=True).tag(sync=True) vpath = Unicode(read_only=True).tag(sync=True) per_kernel_widget_manager_detected = Bool(read_only=True).tag(sync=True) @@ -174,9 +171,9 @@ async def _evaluate(self, options: dict, buffers: list): self.get_namespace(namespace_name, glbls) return {"payload": glbls.get("payload"), "buffers": buffers} - def _context_open_console(self, ref: ShellConnection, active_widget: ShellConnection): + def _context_open_console(self, ref: ShellConnection, current_widget: ShellConnection): "This command is provided for the 'autostart' context menu." - return self.open_console(objects={"ref": ref, "active_widget": active_widget}) + return self.open_console(objects={"ref": ref, "current_widget": current_widget}) def open_console( self, diff --git a/ipylab/sessions.py b/ipylab/sessions.py index c954c0f..4fb8657 100644 --- a/ipylab/sessions.py +++ b/ipylab/sessions.py @@ -3,9 +3,16 @@ from __future__ import annotations +from typing import TYPE_CHECKING + +from traitlets import Unicode + from ipylab.common import Obj from ipylab.ipylab import Ipylab, IpylabBase +if TYPE_CHECKING: + from asyncio import Task + class SessionManager(Ipylab): """ @@ -14,11 +21,16 @@ class SessionManager(Ipylab): SINGLE = True + _model_name = Unicode("SessionManagerModel", help="Name of the model.", read_only=True).tag(sync=True) ipylab_base = IpylabBase(Obj.IpylabModel, "app.serviceManager.sessions").tag(sync=True) - def refresh_running(self): - """Force a call to refresh running sessions.""" - return self.execute_method("refreshRunning") + def get_running(self, *, refresh=True) -> Task[dict]: + "Get a dict of running sessions." + return self.operation("getRunning", {"refresh": refresh}) + + def get_current(self): + "Get the session of the current widget in the shell." + return self.operation("getCurrentSession") def stop_if_needed(self, path): """ diff --git a/ipylab/shell.py b/ipylab/shell.py index 98ca16c..dfccd45 100644 --- a/ipylab/shell.py +++ b/ipylab/shell.py @@ -4,14 +4,14 @@ from __future__ import annotations import inspect -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Unpack from ipywidgets import DOMWidget, TypedTuple, Widget from traitlets import Container, Instance, Unicode import ipylab from ipylab import Area, InsertMode, Ipylab, ShellConnection, Transform, pack -from ipylab.common import Obj +from ipylab.common import IpylabKwgs, Obj, TaskHookType from ipylab.ipylab import IpylabBase if TYPE_CHECKING: @@ -39,6 +39,7 @@ class Shell(Ipylab): _model_name = Unicode("ShellModel", help="Name of the model.", read_only=True).tag(sync=True) ipylab_base = IpylabBase(Obj.IpylabModel, "app.shell").tag(sync=True) + current_widget_id = Unicode(read_only=True).tag(sync=True) connections: Container[tuple[ShellConnection, ...]] = TypedTuple(trait=Instance(ShellConnection)) @@ -53,10 +54,11 @@ def add( ref: ShellConnection | None = None, options: dict | None = None, vpath: str | dict[Literal["title"], str] = "", + hooks: TaskHookType = None, **args, ) -> Task[ShellConnection]: """ - Add a widget or evaluation to the shell. + Add a widget to the shell. obj --- @@ -66,7 +68,8 @@ def add( **Only relevant for 'evaluate'** The 'virtual' path for the app. A new kernel will be created if a session doesn't exist with the same path. - If a dict is provided, a text_dialog will be used to obtain the vpath. + If a dict is provided, a text_dialog will be used to obtain the vpath with the + hook `vpath_getter`. Note: The result (payload) of evaluate must be a Widget with a view and NOT a ShellConnection. @@ -81,7 +84,7 @@ def add( app.shell.add("ipylab.Panel([ipw.HTML('

Test')])", vpath="test") ``` """ - hooks: TaskHooks = {"add_to_tuple_fwd": [(self, "connections")]} + hooks_: TaskHooks = {"add_to_tuple_fwd": [(self, "connections")]} args["options"] = { "activate": activate, "mode": InsertMode(mode), @@ -105,9 +108,9 @@ def add( if c.widget is obj: args["cid"] = c.cid break - hooks["trait_add_fwd"] = [("widget", obj)] + hooks_["trait_add_fwd"] = [("widget", obj)] if isinstance(obj, ipylab.Panel): - hooks["add_to_tuple_fwd"].append((obj, "connections")) + hooks_["add_to_tuple_fwd"].append((obj, "connections")) args["ipy_model"] = obj.model_id if isinstance(obj, DOMWidget): obj.add_class(ipylab.app.selector.removeprefix(".")) @@ -124,11 +127,11 @@ async def add_to_shell() -> ShellConnection: else: args["vpath"] = vpath or ipylab.app.vpath if args["vpath"] != ipylab.app.vpath: - hooks["trait_add_fwd"] = [("auto_dispose", False)] + hooks_["trait_add_fwd"] = [("auto_dispose", False)] else: args["vpath"] = ipylab.app.vpath - return await self.operation("addToShell", {"args": args}, transform=Transform.connection) + return await self.operation("addToShell", {"args": args}, transform=Transform.connection, hooks=hooks_) return self.to_task(add_to_shell(), "Add to shell", hooks=hooks) @@ -143,3 +146,12 @@ def collapse_left(self): def collapse_right(self): return self.execute_method("collapseRight") + + def connect_to_widget(self, widget_id="", **kwgs: Unpack[IpylabKwgs]) -> Task[ShellConnection]: + "Make a connection to a widget in the shell (see also `get_widget_ids`)." + kwgs["transform"] = Transform.connection + return self.operation("getWidget", {"id": widget_id}, **kwgs) + + def list_widget_ids(self, **kwgs: Unpack[IpylabKwgs]) -> Task[dict[Area, list[str]]]: + "Get a mapping of Areas to a list of widget ids in that area in the shell." + return self.operation("getWidgetIds", **kwgs) diff --git a/src/widget.ts b/src/widget.ts index 45b2265..2abf3cf 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -12,7 +12,7 @@ import { PanelModel, PanelView } from './widgets/panel'; import { ShellModel } from './widgets/shell'; import { SplitPanelModel, SplitPanelView } from './widgets/split_panel'; import { TitleModel } from './widgets/title'; - +import { SessionManagerModel } from './widgets/sessions'; export { CommandRegistryModel, ConnectionModel, @@ -24,6 +24,7 @@ export { NotificationManagerModel, PanelModel, PanelView, + SessionManagerModel, ShellConnectionModel, ShellModel, SplitPanelModel, diff --git a/src/widgets/connection.ts b/src/widgets/connection.ts index 6cd30e0..3cc9138 100644 --- a/src/widgets/connection.ts +++ b/src/widgets/connection.ts @@ -6,19 +6,12 @@ import { Signal } from '@lumino/signaling'; import { IpylabModel } from './ipylab'; import { IObservableDisposable } from '@lumino/disposable'; import { Widget } from '@lumino/widgets'; -import { ILabShell } from '@jupyterlab/application'; + /** - * Provides a connection to an object using a unique 'cid'. - * + * ConnectionModel provides a connection to an object using a unique 'cid'. * - * An object must be registered static method `ConnectionModel.registerConnection` - * to establish the connection. The base class expects the object to be - * register before it is created. Subclasses can indicate - * by using a 'pending' promise. The - * The 'cid' can generated in Python first, or generated in the Frontend if a 'cid' - * isn't provided. - * - * The object is set to `this.base`. + * The object to be referenced must first be registered static method + * `ConnectionModel.registerConnection`. */ export class ConnectionModel extends IpylabModel { /** @@ -141,7 +134,7 @@ export class ConnectionModel extends IpylabModel { const cls = obj instanceof Widget && obj.id && - ConnectionModel.getLuminoWidgetFromShell(obj.id) + ConnectionModel.ShellModel.getLuminoWidgetFromShell(obj.id) ? 'ShellConnection' : 'Connection'; const cid = ConnectionModel.new_cid(cls); @@ -155,29 +148,14 @@ export class ConnectionModel extends IpylabModel { } /** - * Get the lumino widget from the shell using its id. - * - * @param id - * @returns + * Get the session associated with the lumino widget if it has one. */ - static getLuminoWidgetFromShell(id: string): Widget | null { - for (const area of [ - 'main', - 'header', - 'top', - 'menu', - 'left', - 'right', - 'bottom' - ]) { - for (const widget of IpylabModel.labShell.widgets( - area as ILabShell.Area - )) { - if (widget.id === id) { - return widget; - } - } + static async getSession(widget: Widget) { + const path = (widget as any)?.ipylabSettings?.vpath; + if (path) { + return await ConnectionModel.sessionManager.findByPath(path); } + return (widget as any)?.sessionContext?.session?.model ?? {}; } static new_cid(cls: string): string { @@ -186,13 +164,14 @@ export class ConnectionModel extends IpylabModel { return `${_PREFIX}${cls}${_SEP}${UUID.uuid4()}`; } + // 'cid' is used by BackboneJS so we use cid_ here. cid_: string; readonly isConnectionModel = true; } /** - * A connection for widgets in the Shell. + * A connection to widgets in the Shell. */ export class ShellConnectionModel extends ConnectionModel { /* @@ -223,6 +202,8 @@ export class ShellConnectionModel extends ConnectionModel { switch (op) { case 'activate': return IpylabModel.app.shell.activateById(this.base.id); + case 'getSession': + return ShellConnectionModel.getSession(this.base); default: return await super.operation(op, payload); } diff --git a/src/widgets/frontend.ts b/src/widgets/frontend.ts index fc5ca26..3d551b2 100644 --- a/src/widgets/frontend.ts +++ b/src/widgets/frontend.ts @@ -31,13 +31,6 @@ export class JupyterFrontEndModel extends IpylabModel { async ipylabInit(base: any = null) { this.set('version', JFEM.app.version); this.set('per_kernel_widget_manager_detected', JFEM.PER_KERNEL_WM); - JFEM.sessionManager.runningChanged.connect(this.updateAllSessions, this); - if (JFEM.labShell) { - JFEM.labShell.currentChanged.connect(this.updateSessionInfo, this); - JFEM.labShell.activeChanged.connect(this.updateSessionInfo, this); - this.updateSessionInfo(); - } - this.updateAllSessions(); this.set('logger_level', this.logger.level); await super.ipylabInit(base); if (!Private.vpathTojfem.has(this.vpath)) { @@ -49,9 +42,6 @@ export class JupyterFrontEndModel extends IpylabModel { close(comm_closed?: boolean): Promise { Private.jfems.delete(this.kernelId); Private.vpathTojfem.delete(this.vpath); - JFEM.labShell.currentChanged.disconnect(this.updateSessionInfo, this); - JFEM.labShell.activeChanged.disconnect(this.updateSessionInfo, this); - JFEM.sessionManager.runningChanged.disconnect(this.updateAllSessions, this); this.logger.stateChanged.disconnect(this.loggerStateChanged as any, this); return super.close(comm_closed); } @@ -77,20 +67,6 @@ export class JupyterFrontEndModel extends IpylabModel { return vpath; } - private updateSessionInfo(): void { - const currentWidget = JFEM.app.shell.currentWidget as any; - const current_session = currentWidget?.sessionContext?.session?.model ?? {}; - if (this.get('current_widget_id') !== currentWidget?.id) { - this.set('current_widget_id', currentWidget?.id ?? ''); - this.set('current_session', current_session); - this.save_changes(); - } - } - private updateAllSessions(): void { - this.set('all_sessions', Array.from(JFEM.sessionManager.running())); - this.save_changes(); - } - private loggerStateChanged(sender: ILogger, change: IStateChange): void { if (this.get('logger_level') !== this.logger.level) { this.set('logger_level', this.logger.level); @@ -142,32 +118,14 @@ export class JupyterFrontEndModel extends IpylabModel { } let kernel: Kernel.IKernelConnection; Private.vpathTojfem.set(vpath, new PromiseDelegate()); - await IpylabModel.app.serviceManager.ready; - await IpylabModel.sessionManager.ready; + await IpylabModel.sessionManager.refreshRunning(); const model = await IpylabModel.sessionManager.findByPath(vpath); if (model) { kernel = IpylabModel.app.serviceManager.kernels.connectTo({ model: model.kernel }); } else { - const sessionContext = new SessionContext({ - sessionManager: IpylabModel.sessionManager, - specsManager: IpylabModel.app.serviceManager.kernelspecs, - path: vpath, - name: vpath, - type: 'console', - kernelPreference: { language: 'python' } - }); - await sessionContext.initialize(); - if (!sessionContext.isReady) { - await new SessionContextDialogs({ - translator: IpylabModel.translator - }).selectKernel(sessionContext!); - } - if (!sessionContext.isReady) { - sessionContext.dispose(); - throw new Error('Cancelling because a kernel was not provided'); - } + const sessionContext = await JFEM.newSessionContext(vpath); kernel = sessionContext.session.kernel; } // Relies on per-kernel widget manager. @@ -191,6 +149,34 @@ export class JupyterFrontEndModel extends IpylabModel { }); } + /** + * Create a new session context for vpath. + * + * This will automatically starting a new kernel if a session path matching + * vpath isn't found. + */ + static async newSessionContext(vpath: string) { + const sessionContext = new SessionContext({ + sessionManager: IpylabModel.sessionManager, + specsManager: IpylabModel.app.serviceManager.kernelspecs, + path: vpath, + name: vpath, + type: 'console', + kernelPreference: { language: 'python' } + }); + await sessionContext.initialize(); + if (!sessionContext.isReady) { + await new SessionContextDialogs({ + translator: IpylabModel.translator + }).selectKernel(sessionContext!); + } + if (!sessionContext.isReady) { + sessionContext.dispose(); + throw new Error('Cancelling because a kernel was not provided'); + } + return sessionContext; + } + /** * Get the WidgetModel * @@ -287,10 +273,11 @@ class IpylabContext { // this.contentsModel.path = path; } readonly path: string; - ready = new Promise(resolve => resolve(null)); - pathChanged = new Signal(this); + ready = new Promise(resolve => resolve(null)); + pathChanged: Signal = new Signal(this); model: object = { stateChanged: new Signal(this) }; localPath = ''; + async rename(newName: string) {} } /** diff --git a/src/widgets/ipylab.ts b/src/widgets/ipylab.ts index 6c71d4f..16feafd 100644 --- a/src/widgets/ipylab.ts +++ b/src/widgets/ipylab.ts @@ -29,12 +29,13 @@ import { } from '../utils'; import { MODULE_NAME, MODULE_VERSION } from '../version'; import type { ConnectionModel } from './connection'; -import { JupyterFrontEndModel } from './frontend'; - -// Determine if the per kernel widget manager is available +import type { JupyterFrontEndModel } from './frontend'; +import type { ShellModel } from './shell'; /** - * Base model for common features + * Base model for Ipylab. + * + * Subclass as required but can also be used directly. */ export class IpylabModel extends WidgetModel { /** @@ -371,6 +372,9 @@ export class IpylabModel extends WidgetModel { if (obj?.dispose) { return { cid: IpylabModel.ConnectionModel.get_cid(obj, true) }; } + if (typeof obj?.iterator === 'function') { + return Array.from(obj); + } return await obj; case 'null': return null; @@ -541,6 +545,7 @@ export class IpylabModel extends WidgetModel { static tracker = new WidgetTracker({ namespace: 'ipylab' }); static JFEM: typeof JupyterFrontEndModel; static ConnectionModel: typeof ConnectionModel; + static ShellModel: typeof ShellModel; static Notification = Notification; static PER_KERNEL_WM = Boolean((KernelWidgetManager as any)?.getManager); } diff --git a/src/widgets/sessions.ts b/src/widgets/sessions.ts new file mode 100644 index 0000000..dd8c120 --- /dev/null +++ b/src/widgets/sessions.ts @@ -0,0 +1,34 @@ +// Copyright (c) ipylab contributors +// Distributed under the terms of the Modified BSD License. + +// SessionManager exposes `app.serviceManager.sessions` to user python kernel + +import { IpylabModel } from './ipylab'; + +/** + * The model for a Session Manager + */ +export class SessionManagerModel extends IpylabModel { + /** + * The default attributes. + */ + defaults(): any { + return { ...super.defaults(), _model_name: SessionManagerModel }; + } + + async operation(op: string, payload: any): Promise { + switch (op) { + case 'getCurrentSession': + return await SessionManagerModel.ConnectionModel.getSession( + IpylabModel.labShell.currentWidget + ); + case 'getRunning': + if (payload.refresh) { + await IpylabModel.sessionManager.refreshRunning(); + } + return Array.from(IpylabModel.sessionManager.running()); + default: + return await super.operation(op, payload); + } + } +} diff --git a/src/widgets/shell.ts b/src/widgets/shell.ts index 8d9c0b9..6f4cec8 100644 --- a/src/widgets/shell.ts +++ b/src/widgets/shell.ts @@ -5,6 +5,17 @@ import { DocumentWidget } from '@jupyterlab/docregistry'; import { UUID } from '@lumino/coreutils'; import { Widget } from '@lumino/widgets'; import { IpylabModel } from './ipylab'; +import { ILabShell } from '@jupyterlab/application'; + +const AREAS: Array = [ + 'main', + 'left', + 'right', + 'header', + 'top', + 'menu', + 'bottom' +]; export class ShellModel extends IpylabModel { /** @@ -14,10 +25,35 @@ export class ShellModel extends IpylabModel { return { ...super.defaults(), _model_name: 'ShellModel' }; } + setReady(): void { + this.base.currentChanged.connect(this._currentChanged, this); + super.setReady(); + } + + _currentChanged() { + const id = this.base.currentWidget?.id; + if (id && id !== this.get('current_widget_id')) { + this.set('current_widget_id', id); + this.save_changes(); + } + } + + close(comm_closed?: boolean): Promise { + this.base.currentChanged.disconnect(this._currentChanged, this); + return super.close(comm_closed); + } + async operation(op: string, payload: any): Promise { switch (op) { case 'addToShell': return await ShellModel.addToShell(payload.args); + case 'getWidget': + if (!payload.id) { + return this.base.currentWidget; + } + return ShellModel.getLuminoWidgetFromShell(payload.id); + case 'getWidgetIds': + return ShellModel.listWidgetIds(); default: return await super.operation(op, payload); } @@ -49,14 +85,15 @@ export class ShellModel extends IpylabModel { /** * Add a widget to the application shell. * - * This function can handle ipywidgets and native Widgets and be used to move - * widgets about the shell. + * This function can handle ipywidgets and native Widgets and be used to + * move widgets about the shell. * - * Ipywidgets are added to a tracker enabling restoration from a - * running kernel such as page refreshing and switching workspaces. + * Ipywidgets are added to a tracker enabling restoration from a running + * kernel such as page refreshing and switching workspaces. * * Generative widget creation is supported with 'evaluate' using the same - * code as 'evalute'. The evaluated code MUST return a widget with a view to be valid. + * code as 'evalute'. The evaluated code MUST return a widget with a view + * to be valid. * * @param args An object with area, options, cid, id, vpath & evaluate. */ @@ -65,42 +102,76 @@ export class ShellModel extends IpylabModel { let widget: Widget | MainAreaWidget; try { - widget = await IpylabModel.toLuminoWidget(args); + widget = await ShellModel.toLuminoWidget(args); // Create a new lumino widget } catch (e) { if (args.evaluate) { // Evaluate code in python to get a panel and then add it to the shell. - const jfem = await IpylabModel.JFEM.getModelByVpath(args.vpath); + const jfem = await ShellModel.JFEM.getModelByVpath(args.vpath); return await jfem.scheduleOperation('shell_eval', args, 'object'); } else { throw e; } } + args.cid = + args.cid || ShellModel.ConnectionModel.new_cid('ShellConnection'); if (args.asDocument && !(widget instanceof DocumentWidget)) { - const jfem = await IpylabModel.JFEM.getModelByVpath(args.vpath); + const jfem = await ShellModel.JFEM.getModelByVpath(args.vpath); const context = jfem.context as any; widget.addClass('ipylab-Document'); const w = (widget = new DocumentWidget({ context, content: widget })); w.node.removeChild(w.toolbar.node); + w.id = args.cid; } - args.cid = - args.cid || IpylabModel.ConnectionModel.new_cid('ShellConnection'); - IpylabModel.ConnectionModel.registerConnection(args.cid, widget); + ShellModel.ConnectionModel.registerConnection(args.cid, widget); widget.id = widget.id || args.cid || UUID.uuid4(); - IpylabModel.app.shell.add(widget as any, args.area || 'main', args.options); + ShellModel.app.shell.add(widget as any, args.area || 'main', args.options); // Register widgets originating from IpyWidgets if (args.ipy_model) { - if (!IpylabModel.tracker.has(widget)) { + if (!ShellModel.tracker.has(widget)) { (widget as any).ipylabSettings = args; - IpylabModel.tracker.add(widget); + ShellModel.tracker.add(widget); } else { (widget as any).ipylabSettings.area = args.area; (widget as any).ipylabSettings.options = args.options; - IpylabModel.tracker.save(widget); + ShellModel.tracker.save(widget); } } return widget; } + + /** + * Get the lumino widget from the shell using its id. + * + * @param id + */ + static getLuminoWidgetFromShell(id: string): Widget | null { + for (const area of AREAS) { + for (const widget of ShellModel.labShell.widgets(area)) { + if (widget.id === id) { + return widget; + } + } + } + } + + /** + * Get mapping of area to array widget ids for all areas. + */ + static listWidgetIds(): any { + const data = Object.create(null); + for (const area of AREAS) { + const items: Array = []; + data[area] = items; + for (const widget of ShellModel.labShell.widgets(area)) { + items.push(widget.id); + } + } + return data; + } + readonly base: ILabShell; } + +IpylabModel.ShellModel = ShellModel;