diff --git a/schema/plugin.json b/schema/plugin.json index 2fc07a7..d02c490 100644 --- a/schema/plugin.json +++ b/schema/plugin.json @@ -39,6 +39,19 @@ "title": "Trigger only for the last selected notebook cell execution.", "description": "Trigger a notification only for the last selected executed notebook cell.", "default": false + }, + "notification_methods": { + "type": "array", + "minItems": 1, + "items": { + "enum": [ + "browser", + "ntfy" + ] + }, + "title": "Notification Methods", + "description": "Methods how to notificate messages. Select from 'browser' or 'ntfy'", + "default": ["browser"] } } } diff --git a/src/index.ts b/src/index.ts index ac2afd2..469787a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; +import { ISessionContext, SessionContext } from '@jupyterlab/apputils'; import { KernelError, Notebook, NotebookActions } from '@jupyterlab/notebook'; import { Cell } from '@jupyterlab/cells'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; @@ -9,6 +10,7 @@ import { ICodeCellModel } from '@jupyterlab/cells'; import { PageConfig } from '@jupyterlab/coreutils'; import LRU from 'lru-cache'; import moment from 'moment'; +import { issueNtfyNotification } from './ntfy'; import { checkBrowserNotificationSettings } from './settings'; interface ICellExecutionMetadata { @@ -19,7 +21,7 @@ interface ICellExecutionMetadata { /** * Constructs notification message and displays it. */ -function displayNotification( +async function displayNotification( cellDuration: string, cellNumber: number, notebookName: string, @@ -27,8 +29,10 @@ function displayNotification( reportCellExecutionTime: boolean, failedExecution: boolean, error: KernelError | null, - lastCellOnly: boolean -): void { + lastCellOnly: boolean, + notificationMethods: string[], + sessionContext: ISessionContext | null, +): Promise { const base = PageConfig.getBaseUrl(); const notificationPayload = { icon: base + 'static/favicon.ico', @@ -52,13 +56,20 @@ function displayNotification( } notificationPayload.body = message; - new Notification(title, notificationPayload); + + if (notificationMethods.includes('browser')) { + new Notification(title, notificationPayload); + } + if ((notificationMethods.includes('ntfy')) && (sessionContext)) { + await issueNtfyNotification(title, notificationPayload, sessionContext); + } + } /** * Trigger notification. */ -function triggerNotification( +async function triggerNotification( cell: Cell, notebook: Notebook, cellExecutionMetadataTable: LRU, @@ -69,7 +80,9 @@ function triggerNotification( cellNumberType: string, failedExecution: boolean, error: KernelError | null, - lastCellOnly: boolean + lastCellOnly: boolean, + notificationMethods: string[], + sessionContext: ISessionContext | null, ) { const cellEndTime = new Date(); const codeCellModel = cell.model as ICodeCellModel; @@ -102,7 +115,7 @@ function triggerNotification( ? cellExecutionMetadata.index : codeCellModel.executionCount; const notebookName = notebook.title.label.replace(/\.[^/.]+$/, ''); - displayNotification( + await displayNotification( cellDuration, cellNumber, notebookName, @@ -110,7 +123,9 @@ function triggerNotification( reportCellExecutionTime, failedExecution, error, - lastCellOnly + lastCellOnly, + notificationMethods, + sessionContext, ); } } @@ -128,6 +143,8 @@ const extension: JupyterFrontEndPlugin = { let reportCellNumber = true; let cellNumberType = 'cell_index'; let lastCellOnly = false; + let notificationMethods = ['browser']; + const cellExecutionMetadataTable: LRU< string, ICellExecutionMetadata @@ -138,6 +155,13 @@ const extension: JupyterFrontEndPlugin = { max: 500 }); + // SessionContext is used for running python codes + const manager = app.serviceManager; + const sessionContext = new SessionContext({ + sessionManager: manager.sessions as any, + specsManager: manager.kernelspecs, + }); + if (settingRegistry) { const setting = await settingRegistry.load(extension.id); const updateSettings = (): void => { @@ -150,6 +174,7 @@ const extension: JupyterFrontEndPlugin = { .composite as boolean; cellNumberType = setting.get('cell_number_type').composite as string; lastCellOnly = setting.get('last_cell_only').composite as boolean; + notificationMethods = setting.get('notification_methods').composite as string[]; }; updateSettings(); setting.changed.connect(updateSettings); @@ -165,10 +190,10 @@ const extension: JupyterFrontEndPlugin = { } }); - NotebookActions.executed.connect((_, args) => { + NotebookActions.executed.connect(async (_, args) => { if (enabled && !lastCellOnly) { const { cell, notebook, success, error } = args; - triggerNotification( + await triggerNotification( cell, notebook, cellExecutionMetadataTable, @@ -179,16 +204,18 @@ const extension: JupyterFrontEndPlugin = { cellNumberType, !success, error, - lastCellOnly + lastCellOnly, + notificationMethods, + sessionContext, ); } }); - NotebookActions.selectionExecuted.connect((_, args) => { + NotebookActions.selectionExecuted.connect(async (_, args) => { if (enabled && lastCellOnly) { const { lastCell, notebook } = args; const failedExecution = false; - triggerNotification( + await triggerNotification( lastCell, notebook, cellExecutionMetadataTable, @@ -199,7 +226,9 @@ const extension: JupyterFrontEndPlugin = { cellNumberType, failedExecution, null, - lastCellOnly + lastCellOnly, + notificationMethods, + sessionContext, ); } }); diff --git a/src/ntfy.ts b/src/ntfy.ts new file mode 100644 index 0000000..33358d8 --- /dev/null +++ b/src/ntfy.ts @@ -0,0 +1,35 @@ +import { ISessionContext } from '@jupyterlab/apputils'; +import { Kernel, KernelAPI, KernelMessage } from '@jupyterlab/services'; + +export async function ensureSessionContextKernelActivated( + sessionContext: ISessionContext +): Promise { + if (sessionContext.hasNoKernel) { + await sessionContext.initialize() + .then(async (value) => { + if (value) { + const py3kernel = await KernelAPI.startNew({ name: 'python3' }); + await sessionContext.changeKernel(py3kernel); + } + }) + .catch((reason) => { + console.error(`Failed to initialize the session in jupyterlab-notifications.\n${reason}`); + });; + } +} + +export async function issueNtfyNotification( + title: string, + notificationPayload: { body: string }, + sessionContext: ISessionContext, +): Promise> { + const { body } = notificationPayload; + await ensureSessionContextKernelActivated(sessionContext); + if (!sessionContext || !sessionContext.session?.kernel) { + return; + } + const titleEscaped = title.replace(/"/g, '\\"'); + const bodyEscaped = body.replace(/"/g, '\\"'); + const code = `from ntfy import notify; notify("${bodyEscaped}", "${titleEscaped}")`; + return sessionContext.session?.kernel?.requestExecute({ code }); +}