How a notebook behaves once it is open.

Server dashboard

M-x emjupy-server-dashboard (also emjupy-notebook-list) opens one buffer per server showing what it has: the kernels running on it and the notebooks stored on it, browsable into subdirectories.

Key Effect
RET open the notebook, or descend into the folder
^ up a directory (the .. row)
g re-fetch from the server
k shut down the kernel on this line
K shut down every kernel on this server
n create a notebook in the folder being shown (with one empty cell)
d hand this directory to Dired

It is built on tabulated-list-mode, so sorting and navigation are Emacs's own. It is deliberately not a file manager: d hands the directory to Dired, over TRAMP when the server is remote. The Contents API says what notebooks exist but not how to reach them as files – and a tunnelled server looks like localhost from here – so tell emjupy where they live:

elisp
(setq emjupy-remote-root "/ssh:user@host:/home/user/notebooks")
;; or per server:
(setq emjupy-remote-root
      '(("localhost:8888" . "~/notebooks")
        ("localhost:9999" . "/ssh:box:/srv/nb")))

Editing while a cell runs

A slow cell shows a marker where its execution count will go – [In: |], cycling – so a cell that is running looks different from one that was never run. emjupy-running-indicator sets the frames; a single-element vector like ["*"] gives Jupyter's static marker instead. The animation moves only overlay strings, never buffer text, so it cannot disturb what you are typing or land in the undo history.

You can edit anywhere in the notebook while a cell runs. Output is drawn into its own cell's box and nowhere else, so text typed in the meantime – in that cell's source or any other – is left as it is, and so is point.

If the display ever does look wrong, C-c C-x C-l (emjupy-re-render) rebuilds it from the cells. It keeps pending edits, and never touches the notebook on the server.

Editable regions

Only cell source text is editable. Rules, the gutters between cells and output boxes are read-only, because text entered there belongs to no cell and would be discarded at the next redraw.

Editing at cell boundaries is unaffected: the end of a cell's last line and the start of the next cell are both within cells.

Set emjupy-protect-non-cell-regions to nil to disable this.

Indentation, tracebacks, and finding your own code

TAB indents as python-mode would, and offers the alternatives on repeat: after def f(): the next line might be the body or back at the outer level, so pressing it again cycles. emjupy asks python-mode rather than guessing, using the cell as the context.

C-$ opens the cell's full traceback in its own buffer, with Python highlighting and the colours the kernel sent – the output box squeezes a deep stack into a few lines, which is where tracebacks become unreadable.

M-. on a function from one of your own modules needs the language server to read the notebook's code as if it sat in the notebook's own directory: a language server resolves import mylib relative to the file it is reading.

emjupy asks the kernel where it runs – kernels are started through a Jupyter session bound to the notebook, which starts them in the notebook's directory – and presents the code to the server as a file there. Nothing needs configuring, for a kernel on this machine or another, provided the language server runs beside the kernel; see Language server support.

Files emjupy writes

For a notebook on this machine, emjupy keeps a hidden file beside it, named after the server and the notebook – for test.ipynb on localhost:8888, .emjupy_localhost_8888__test.ipynb.py. It is the notebook's code as one Python file, for the language server to read, and it is written once, when the notebook's language support starts.

It is safe to delete, and is rebuilt the next time it is needed; add .emjupy_* to a project's .gitignore. Nothing else is written there: no lock file, no auto-save file. Set emjupy-shadow-beside-notebook to nil to keep it in a temporary directory instead, or emjupy-shadow-directory to choose one – at the cost, without a language server beside the kernel, of M-. into your own modules.

For a notebook whose kernel runs elsewhere the file goes to a temporary directory on this machine, and nothing is written on the remote.

A notebook with unsaved edits is also copied to emjupy-recovery-directory until it is saved – see Saving, and not losing work. Those copies are readable by you alone; delete them once you no longer want them.

Hiding output

C-c C-o collapses the output of the cell at point, and restores it. A collapsed cell shows its bottom rule marked:

└─ ▼ 3 outputs hidden ─────────────────────────────┘

The marker is clickable: mouse-1 on the triangle shows the output again. Only the triangle is live – it highlights under the pointer, and the rest of the rule is an ordinary line. In a terminal this needs xterm-mouse-mode; the key binding works either way.

Nothing is discarded. The outputs stay on the cell, so restoring them is a redraw and never a re-run, and the kernel is not involved either way. This is the difference between hiding output and clearing it.

The state is kept at metadata.jupyter.outputs_hidden, where Jupyter keeps it, so it survives saving and means the same thing in other front ends.

Clearing output

C-c C-l discards the output of the cell at point; C-c C-u C-l does it for the whole notebook, asking first, since re-running everything is not always cheap.

Both clear the execution count along with the output – leaving [In: 4] beside an empty box would claim something had been run since – and both touch only the buffer. The notebook on the server keeps its outputs until you save, so clearing is not a way to lose results by accident.

Clearing everything before committing is the usual reason to want this: it strips the outputs that make .ipynb diffs unreadable.

Output rendering

Output is redrawn at most once per emjupy-output-render-interval seconds (0.15 by default), however many messages arrive, and only in the cell it belongs to. Redrawing on each message is impractical: a progress bar sends one message per iteration, and a cell holding figures takes tens of milliseconds to draw.

M-x emjupy-flush-output forces a pending redraw.

Stream output continues the output it belongs to rather than starting a new one, as in Jupyter, so a cell that prints a thousand lines has one output, not a thousand.

Saving, and not losing work

C-x C-s saves the notebook to its server. It is also saved on its own, as Jupyter does: every emjupy-autosave-interval seconds (120) of a notebook with unsaved edits, when Emacs is idle.

A save never overwrites a copy changed elsewhere. If the notebook was saved from JupyterLab, or by someone else, since it was opened here, C-x C-s asks before overwriting it, and auto-saving it stops, saying so, until it is saved by hand.

Unsaved edits are never only in the buffer. A few seconds after a change, when Emacs is idle, the notebook is copied to emjupy-recovery-directory (/tmp/emjupy-recovery-USER/ by default), and again as Emacs quits. The copy is local, so it is written with the tunnel down or the server gone, and a save deletes it. Opening the notebook again, after a crash say, offers to restore it; M-x emjupy-recover-notebook does too. A copy you decline is set aside in that directory, not deleted.

Many systems empty /tmp when the machine restarts, so by default a copy outlives Emacs crashing but not the machine. For one that outlives both, set emjupy-recovery-directory to a directory under your home, such as ~/.emacs.d/emjupy-recovery/. Copies are only written into a directory you own that is not a symbolic link, so another user of a shared /tmp cannot have them written elsewhere.

Closing a notebook with unsaved edits asks whether to save them, keep them in the recovery copy, or discard them. Quitting Emacs offers to save every such notebook. And opening a notebook already open with unsaved edits shows it as it is, rather than fetching it again.

Interactive output

Figures

A figure that draws itself in JavaScript – plotly, bokeh – has nothing to show in a text buffer, and shows as a line saying what it is. C-c C-f there, a click, or RET opens it in a window of its own, one per figure, not a tab in your browser. That window is the small WebKitGTK viewer that comes with emjupy where Python has PyGObject and WebKitGTK – GNOME desktops do, and the system's Python is tried when the one first on the PATH is a virtualenv's – or else a new Firefox window, or a Chromium app window: emjupy-figure-window-browsers lists the browsers tried, in order. emjupy-figure-window-command names another program; emjupy-figure-viewer can choose the browser instead, or an xwidget, inside Emacs – see Known limitations for why that is not the default. Opening is always asked for, never automatic: it runs the notebook's JavaScript.

interactive-plotly.gif
Figure 1: Opening a plotly figure, and rotating it.

A plotly figure arrives as its JSON specification, so the page is built in Emacs, with plotly.js asked of the kernel that made the figure: it matches the figure, needs no network, and works through a tunnel. The first figure of a plotly version takes some seconds – the server limits how fast output reaches Emacs, so it comes in pieces – and later ones none: it is kept. emjupy-plotly-js names a URL or a file instead.

C-c C-f on an image opens it in a frame of its own, for a figure too small to read at cell width; the same frame each time. With emjupy-inline-figures set to nil, figures are not drawn in the notebook at all: each is a line, opened there the same way. Drawn inline is the default.

Widgets

ipywidgets controls show as controls. An interact slider reads frequency ◀ 3 ▶ 1 … 10: <left> and <right> anywhere on it step it, the arrows can be clicked, and RET on the value asks for one. Moving it redraws the figure in place, as in a notebook.

Every widget is drawn from the shape of its state, not from its type: a number with bounds and a step is stepped (◀ 3 ▶), one without a step is a bar, two numbers are a range, option labels are chosen from (a ▾), true or false is a box to toggle ([x]), a value with no bounds is edited in brackets ([text]), a button is clicked ([ Press ]), and a box draws what it holds. So ipywidgets' controls all show without emjupy naming them – 49 of its 50 widgets, checked against its own list – and a new one does too if its state has a familiar shape. RET or a click on a control uses it; what it asks for in the minibuffer follows the value: a number within its bounds, text, a password, a list, a date or a time, a colour.

interactive-widgets.gif
Figure 2: An interact slider, stepped from 3 to 6.

Widgets whose data is binary work too: an image widget is drawn in the notebook, and drawn again when the kernel replaces it (or shown as a line that opens it, with emjupy-inline-figures nil); a sound or a video plays in a window of its own; a file upload asks for files and sends them.

A widget that draws itself in JavaScript – plotly's FigureWidget, a map, an anywidget – shows as a line, ▶ Interactive widget: FigureWidget (anywidget), and opens in a page of its own, in the figure window, where its JavaScript draws it, live: a change made there reaches the kernel, and one made in the kernel reaches the page. The page talks to Emacs, which relays over the kernel connection it has: it needs no token, and works through a tunnel as the notebook does.

The widget's code is asked of the kernel – widget packages leave a copy for the classic notebook in its environment, which matches its version and needs no network; one with none is fetched from a CDN. The widget manager itself comes from a CDN: emjupy-widget-scripts names local copies instead, for a machine with no network.

Not drawn: a gamepad, shown by name.

Progress bars

A progress bar redraws itself by returning to the start of the line with a carriage return and writing over what was there. emjupy collapses those the way a terminal does, so a bar occupies one line and shows its latest state.

What appears depends on what the program actually wrote, and tqdm writes a frame only every mininterval seconds, 0.1 by default. A loop that finishes faster than that produces exactly two frames: the opening one, at 0% with the bar area blank, and the closing one at 100%. Nothing was lost – nothing else was ever sent.

Each stream keeps a line of its own: a bar on stderr is redrawn in place while a print in the same loop, on stdout, goes to its own lines. The bar shows once, in its latest state, with the printed lines together below it.

Wide output

No line of output is drawn wider than the cell. The kernel is told the cell's width in COLUMNS when it attaches, before a cell runs if the width has changed, and when the window is resized – which is how tqdm, and anything using shutil.get_terminal_size, learn how wide to draw when their output is not a terminal.

Output that is wider anyway – a long log line, a wide table, a program that ignores COLUMNS – is broken where the cell ends. Only the drawing changes: the cell keeps its output as the kernel sent it, so a saved notebook holds the original lines. A resize redraws the cells whose lines were broken, at the new width.

ANSI colour in output

Output from termcolor, rich or colorama arrives with ANSI escapes in it. These are applied as real colours rather than shown as \\033[33m; set emjupy-render-ansi-colors to nil to strip them instead.

Markdown highlighting

Markdown cells are highlighted out of the box – headings, emphasis, inline code and fences, links, list markers and blockquotes – with no external package. markdown-mode and markdown-ts-mode are MELPA packages emjupy does not depend on; when one of them IS installed it is used in preference to the built-in approximation.

Copying between cells

Text copied out of a cell carries its faces but none of emjupy's own properties. One of them, emjupy-cell, holds the cell itself – outputs, overlay and all – and carried into the kill ring it would make text pasted elsewhere claim to belong to a cell it has nothing to do with.

A kill whose region extends past the end of a cell is refused, but the text still reaches the kill ring: Emacs copies before deleting. The error is therefore accompanied by a usable C-y.

Relative paths in cells

Kernels are started through a Jupyter session bound to the notebook, so a cell's working directory is the notebook's own folder, as it is in Jupyter: open("data.csv") finds the file next to the notebook.

Exporting to a plain .py

C-c C-x C-e (emjupy-export-py) writes the notebook out as Python in percent format: # %% before each code cell, and markdown cells carried across as commented # %% [markdown] blocks. That is the format Jupytext, VS Code and Spyder already read, so the result opens as a notebook again elsewhere rather than being a one-way dump.

The default destination is beside the notebook itself, on the server it came from. The file is written through the same Contents API the notebook is read through, so this works for a remote kernel with no TRAMP and no configuration – the .py lands next to the .ipynb on that machine. With a prefix argument it writes to a local file instead, and a TRAMP file name works there if you want it somewhere else again.

The buffer is synced first, so unsaved edits are included, and an existing file is never overwritten without asking. Set emjupy-export-markdown-cells to nil to export code only.

Note this is not the shadow buffer dumped to disk, although that is where the idea came from: the shadow buffer's markers carry emjupy's internal cell ids, which mean nothing outside emjupy, and it holds code cells only – so exporting it directly would silently drop your prose.

Which version am I running?

M-x emjupy-version reports the version, where the code was loaded from and, in a checkout, the revision. In a notebook buffer it also names the kernel's working directory and which language server is answering:

emjupy 0.1.12, git 3e8860a (2026-09-09), loaded from ~/.emacs.d/elpa/emjupy/;
kernel cwd /home/you/project; language server beside the kernel

"On this machine" instead means the fallback server is answering: for a kernel that runs elsewhere, M-. will not find your own modules. See Language server support.