diff --git a/IPython/html/static/notebook/js/actions.js b/IPython/html/static/notebook/js/actions.js index f91091d76..db11852d7 100644 --- a/IPython/html/static/notebook/js/actions.js +++ b/IPython/html/static/notebook/js/actions.js @@ -253,6 +253,14 @@ define(['require' env.notebook.delete_cell(); } }, + 'toggle-unsolicited-message-display':{ + help: 'toggle display from external clients', + icon: 'fa-sitemap', + help_index: 'gb', + handler: function (env) { + env.notebook.toggle_ignore_unsolicited_msgs(); + } + }, 'interrupt-kernel':{ icon: 'fa-stop', help_index : 'ha', diff --git a/IPython/html/static/notebook/js/keyboardmanager.js b/IPython/html/static/notebook/js/keyboardmanager.js index 4c6323f0b..06f6c834a 100644 --- a/IPython/html/static/notebook/js/keyboardmanager.js +++ b/IPython/html/static/notebook/js/keyboardmanager.js @@ -95,6 +95,7 @@ define([ 'space' : 'ipython.scroll-down', 'down' : 'ipython.select-next-cell', 'i,i' : 'ipython.interrupt-kernel', + 'e': 'ipython.toggle-unsolicited-message-display', '0,0' : 'ipython.restart-kernel', 'd,d' : 'ipython.delete-cell', 'esc': 'ipython.close-pager', diff --git a/IPython/html/static/notebook/js/menubar.js b/IPython/html/static/notebook/js/menubar.js index 9a2814b10..c919a9e33 100644 --- a/IPython/html/static/notebook/js/menubar.js +++ b/IPython/html/static/notebook/js/menubar.js @@ -278,6 +278,9 @@ define([ }); // Kernel + this.element.find('#toggle_unsolicited').click(function() { + IPython.notebook.toggle_ignore_unsolicited_msgs(); + }); this.element.find('#int_kernel').click(function () { that.notebook.kernel.interrupt(); }); diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index 09844a927..cce807943 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -135,6 +135,7 @@ define([ this.undelete_below = false; this.paste_enabled = false; this.writable = false; + this.ignore_unsolicited_msgs = false; // It is important to start out in command mode to match the intial mode // of the KeyboardManager. this.mode = 'command'; @@ -1562,6 +1563,60 @@ define([ this.get_selected_cell().toggle_line_numbers(); }; + // Support for displaying input and output messages from other iPy clients. + + /** + * Toggles the ability to display input/output message events from + * externally connected clients (i.e. other iPython shells, vim-ipython, + * etc). + * + * @method toggle_ignore_unsolicited_msgs + */ + Notebook.prototype.toggle_ignore_unsolicited_msgs = function () { + this.ignore_unsolicited_msgs = !this.ignore_unsolicited_msgs; + this.events.trigger('toggle_unsolicited_msgs.Notebook', + [this.ignore_unsolicited_msgs]); + return this.ignore_unsolicited_msgs; + }; + + /** + * Handles the display of unsolicited messages, i.e. inputs or outputs that + * were generated by a client other than this notebook. New messages are + * displayed at the bottom of the notebook. + * + * @method handle_unsolicited_msg + */ + Notebook.prototype.handle_unsolicited_msg = function(msg) { + if (this.ignore_unsolicited_msgs) { + return; + } + if (msg.msg_type == 'execute_input') { + var cell = this.insert_cell_at_bottom('code'); + if (cell) { + var cell_index = this.ncells() - 1; + cell.last_msg_id = msg.parent_header.msg_id; + cell.set_text(msg.content.code); + cell._handle_execute_reply(msg); + this.scroll_to_cell(cell_index); + this.select(cell_index); + } + } else { + /* Find the input cell that corresponds with the output, then add + * the contents to the cell's output area. + */ + var count = this.ncells(); + while (count--) { + var cell = this.get_cell(count); + if (cell && cell.last_msg_id == msg.parent_header.msg_id) { + cell.output_area.handle_output(msg); + this.scroll_to_cell(count); + this.select(count); + break; + } + } + } + }; + /** * Set the codemirror mode for all code cells, including the default for * new code cells. @@ -1639,6 +1694,7 @@ define([ cell.set_kernel(this.session.kernel); } } + this.kernel.unsolicited_msg_callback = $.proxy(this.handle_unsolicited_msg, this); }; Notebook.prototype._session_start_failed = function (jqxhr, status, error){ this._session_starting = false; diff --git a/IPython/html/static/notebook/js/notificationarea.js b/IPython/html/static/notebook/js/notificationarea.js index 2cc43718e..842a6a512 100644 --- a/IPython/html/static/notebook/js/notificationarea.js +++ b/IPython/html/static/notebook/js/notificationarea.js @@ -279,6 +279,11 @@ define([ nnw.warning(error.message || "Notebook copy failed"); }); + this.events.on('toggle_unsolicited_msgs.Notebook', function(evt, ignored) { + var msg = (ignored? "Ignoring": "Showing") + " I/O from external clients"; + nnw.set_message(msg, 1000); + }); + // Checkpoint events this.events.on('checkpoint_created.Notebook', function (evt, data) { var msg = "Checkpoint created"; diff --git a/IPython/html/static/services/kernels/kernel.js b/IPython/html/static/services/kernels/kernel.js index 77cf4b1f5..28b77ca47 100644 --- a/IPython/html/static/services/kernels/kernel.js +++ b/IPython/html/static/services/kernels/kernel.js @@ -47,6 +47,7 @@ define([ this.session_id = utils.uuid(); this._msg_callbacks = {}; this.info_reply = {}; // kernel_info_reply stored here after starting + this.unsolicited_msg_callback = null; if (typeof(WebSocket) !== 'undefined') { this.WebSocket = WebSocket; @@ -136,6 +137,7 @@ define([ this._iopub_handlers = {}; this.register_iopub_handler('status', $.proxy(this._handle_status_message, this)); this.register_iopub_handler('clear_output', $.proxy(this._handle_clear_output, this)); + this.register_iopub_handler('execute_input', $.proxy(this._handle_input_message, this)); for (var i=0; i < output_msg_types.length; i++) { this.register_iopub_handler(output_msg_types[i], $.proxy(this._handle_output_message, this)); @@ -994,6 +996,11 @@ define([ Kernel.prototype._handle_output_message = function (msg) { var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id); if (!callbacks || !callbacks.iopub) { + if (this.unsolicited_msg_callback) { + // The message came from another client. Let the UI decide what + // to do with it. + this.unsolicited_msg_callback(msg); + } return; } var callback = callbacks.iopub.output; @@ -1002,6 +1009,20 @@ define([ } }; + /** + * Handle an input message (execute_input). + * + * @function _handle_input message + */ + Kernel.prototype._handle_input_message = function (msg) { + var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id); + if (!callbacks && this.unsolicited_msg_callback) { + // The message came from another client. Let the UI decide what to + // do with it. + this.unsolicited_msg_callback(msg); + } + }; + /** * Dispatch IOPub messages to respective handlers. Each message * type should have a handler. diff --git a/IPython/html/templates/notebook.html b/IPython/html/templates/notebook.html index 1f8b554c7..242303035 100644 --- a/IPython/html/templates/notebook.html +++ b/IPython/html/templates/notebook.html @@ -228,6 +228,9 @@ class="notebook_app"