Skip to content

Repository files navigation

jovian.nvim 🪐

Lua Rust Neovim

jovian.nvim turns Neovim into a Jupyter-like environment for Python. Edit .py files using the # %% cell format, execute code against a live IPython kernel, and view rich output — text, tables, errors, and plots — inline below each cell or in a side-by-side preview window, without leaving your editor.

Execution is powered by jovian-core, a small Rust backend that speaks the Jupyter wire protocol directly. No libzmq, no pyzmq, no Python bridge process — just a single static binary talking msgpack-RPC to Neovim.


📖 Table of Contents


✨ Features

Feature Description
🦀 Rust core A single jovian-core binary speaks the Jupyter wire protocol directly over msgpack-RPC. No libzmq/pyzmq/Python-bridge dependencies.
🚀 Low latency Streamed stdout/stderr, cell status, and completions come straight off the kernel's IOPUB/SHELL sockets.
👀 Live preview Rich output (text, tables, errors, plots) in a side-by-side window, rendered from a structured output store. DataFrames keep every column supplied by Pandas and scroll horizontally.
🃏 Inline cell cards Optional bordered "cards" around each cell with output rendered directly beneath it, à la notebook UIs.
📝 Markdown cells Optional styling for # %% [markdown] cells: headings, bold/italic, code spans, bullets, and aligned tables.
🖼️ Inline plots Image MIME output such as Matplotlib PNGs rendered in-terminal via the Kitty graphics protocol (Kitty, Ghostty, recent WezTerm).
📊 Variables & DataFrames :JovianVars and :JovianView inspect the live namespace and paginate DataFrames.
⚡ Quick eval / REPL Try throwaway code against the kernel without polluting In[]/Out[] history.
🔁 Persistent outputs Cell output is cached to an nbformat-shaped JSON sidecar, so results survive restarts.
☁️ Remote kernels Run the kernel on an SSH host; jovian-core tunnels its ZMQ ports back to localhost. Key/agent auth.
🪄 Magic commands %timeit, !ls, and other IPython magics work, with LSP false-positives suppressed.

⚡ Try with Nix

No install needed. If you have Nix, the flake bundles the prebuilt jovian-core binary and a minimal Python environment:

nix run github:m-tky/jovian.nvim -- demo_jovian.py

The demo launches with the inline cell cards, markdown styling, and inline output features all enabled so you can see everything at once.


📋 Requirements

Tip

For Nix / NixOS users: everything below — the Rust binary, the Python environment, and a Kitty-capable terminal — is handled by the flake. See Installation (Nix).

1. Python (essential)

A Python 3.10+ environment containing:

  • ipykernel — the kernel jovian connects to.
  • ipython — normally installed alongside ipykernel; jovian's REPL evaluates against this same live kernel and does not require jupyter-console.

That's it for the kernel. There is no pyzmq, jupyter-client, or jupyter-console requirement anymore — jovian-core talks to the kernel directly.

How jovian picks the Python interpreter

By default jovian auto-resolves a usable Python at startup — you don't need to register a Jupyter kernel.json. It probes the following sources in order and picks the first one that can import ipykernel:

  1. $JOVIAN_PYTHON (explicit env var, used as-is)
  2. PATH python3 / python — the typical nix develop, asdf, or system case: drop into a shell that has the right Python, then launch Neovim
  3. $VIRTUAL_ENV/bin/python — active venv
  4. $CONDA_PREFIX/bin/python — active conda env
  5. ./.venv/bin/python, ./venv/bin/python — project-local venvs (poetry, uv, vanilla)

If nothing matches, jovian falls back to the bare string python3 and the Rust core looks for a registered Jupyter kernelspec as a last resort.

To pin a specific interpreter, set python_interpreter explicitly in setup() — the resolver is then skipped and your value is used verbatim (:checkhealth jovian warns if ipykernel is missing instead of overriding your choice). To switch at runtime, run :JovianPickPython and choose from the detected pythons + registered kernelspecs; the kernel is restarted with the new selection.

2. The jovian-core binary

The native backend. You get it one of three ways, in order of preference:

  1. Prebuilt download (default): the plugin's build hook downloads the release binary matching your platform (Linux/macOS, x86_64/aarch64).
  2. Built from source: if no prebuilt matches, the hook runs cargo build --release — this needs a Rust toolchain.
  3. Nix: the flake drops the binary in place; nothing to download or build.

3. Optional

  • A Kitty-graphics terminal (Kitty, Ghostty 1.3+, recent WezTerm) — required for inline plot rendering. Without one, images are skipped; text output is unaffected.
  • nvim-treesitter — for code highlighting and markdown injection inside cells.

📦 Installation

Using Lazy.nvim

The build hook installs jovian-core (prebuilt download, or cargo fallback):

{
    "m-tky/jovian.nvim",
    build = function(plugin)
        require("jovian.install").run(plugin)
    end,
    config = function()
        require("jovian").setup({
            -- python_interpreter is auto-resolved by default — set it
            -- explicitly only to pin a specific interpreter (absolute
            -- path skips Jupyter kernelspec lookup entirely).
            -- python_interpreter = "/path/to/your/python",

            -- Opt-in visuals (all off by default):
            cell_frame = true,           -- bordered cell cards
            markdown_cell_style = true,  -- styled markdown cells
            inline_outputs = true,       -- output rendered below cells
        })
    end,
}

Using Nix (Flake)

The recommended way on Nix is the overlay, which adds jovian-nvim (with the binary bundled) to pkgs.vimPlugins.

# Example integration in your flake.nix
{
  inputs.jovian.url = "github:m-tky/jovian.nvim";

  outputs = { nixpkgs, jovian, ... }: {
    # 1. Apply the overlay
    pkgs = import nixpkgs {
      inherit system;
      overlays = [ jovian.overlays.default ];
    };

    # 2. Add it to your Neovim plugins like any other plugin
    # Example using Home Manager:
    programs.neovim.plugins = [
      pkgs.vimPlugins.jovian-nvim
    ];

    # 3. (Optional) Use the provided minimal Python environment
    # require("jovian").setup({
    #   python_interpreter = "${pkgs.jovian-minimal-python}/bin/python3",
    # })
  };
}

Runtime Autocompletion

Jovian provides context-aware completion powered by the kernel — dictionary keys, DataFrame columns, and dynamic attributes that a static LSP can miss. It is registered as omnifunc (trigger with <C-x><C-o>).

Integration with blink.cmp:

require('blink.cmp').setup({
  sources = {
    default = { 'lsp', 'path', 'snippets', 'buffer', 'jovian' },
    providers = {
      jovian = {
        name = 'Jovian',
        module = 'blink.cmp.sources.omnifunc',
        enabled = true,
      }
    }
  }
})

🎮 Usage Guide

Running code

  • Define cells with # %%. IDs (# %% id="...") are generated on first run.
  • :JovianStart connects a kernel (or it starts lazily on first run).
  • :JovianRun runs the current cell; :JovianRunAndNext, :JovianRunAll, :JovianRunAbove, :JovianRunLine, and :JovianSendSelection cover the rest.
  • No preview or side panel is required to execute code. Open :JovianOpen only when you want the preview/pinned-output panels; results and status still persist without them.
  • Cell headers show virtual-text status: Running…, Done, Error, Stale.

Inspecting data

  • :JovianVars — show the live namespace in a floating window.
  • :JovianView [var] — inspect a DataFrame (paginated, 50 rows/page; use <PageDown> / <PageUp>).
  • DataFrame results below a cell are a compact summary: visible headers are never abbreviated, values may be, and columns are reselected when the source window is resized. The preview keeps all columns supplied by Pandas and uses normal horizontal buffer scrolling (zl / zh).
  • Pandas may omit middle columns before jovian receives the HTML result (the familiar column). That matches JupyterLab behavior; set pd.set_option("display.max_columns", 100) in your kernel when you want a larger, bounded MIME representation. Re-run the cell after changing it.

Quick eval and REPL

  • :JovianEval [code] runs a one-off expression against the kernel with history disabled — it sees all your variables but doesn't bump In[]/Out[] or get recorded.
  • :JovianREPL, or pressing i / e in the Output window, opens an interactive loop on top of the same mechanism: type, run, repeat. An empty line exits. No jupyter console needed.

Output window

By default the Output (REPL) window is on-demand — toggle it with :JovianToggleOutput. Output still accumulates in the background, so the full cross-cell log and live \r progress bars (tqdm) are there when you open it. Set output_window = "always" to dock it, or "off" to drop it entirely.

Remote kernels (SSH)

Run the kernel on a remote host while editing locally. jovian-core starts the kernel over SSH and tunnels its ZMQ ports back to localhost, so everything else (output, plots, completion) works identically.

  1. :JovianConnect — pick a host from ~/.ssh/config (or Tailscale), then enter the remote python and working directory. The host becomes active. (:JovianAddHost registers a host manually; :JovianUse <name> switches.)
  2. :JovianRun — the kernel launches on the remote and output streams back.
  3. :JovianSync [path]rsync your local files to the remote working dir.
  4. :JovianTunnelStatus — show the active host and whether the kernel is up.

Requirements: key- or ssh-agent-based auth (no interactive password prompt), and ipykernel installed in the remote python. Closing Neovim or :JovianRestart tears the remote kernel down cleanly.


🎨 Visual Features

Three independent, opt-in rendering layers. Enable any combination:

require("jovian").setup({
    cell_frame = true,          -- ┌─ [3] Code ──┐ card borders around cells
    markdown_cell_style = true, -- conceal/style # %% [markdown] cells
    inline_outputs = true,      -- render kernel output below each cell
})
  • cell_frame draws a bordered card around each cell (code vs. markdown cells get distinct border colors). It shifts the right edge of cell lines, so it's off by default.
  • markdown_cell_style conceals markdown punctuation (#, **, `) in # %% [markdown] cells and renders headings, bold/italic, code spans, bullets, and box-drawn tables. The raw source re-appears on the cursor line while you edit. It also renders inline images in markdown cells: a ![alt](data:image/png;base64,…) data URI (e.g. a screenshot pasted into a notebook and exported with jupytext — the long base64 is hidden) or a local ![alt](figs/plot.png) file path. Images need a Kitty-graphics terminal; without one the base64 is still hidden so it never shows as raw text.
  • inline_outputs renders each cell's output (stdout/stderr, text results, error tracebacks, and Kitty images) as virtual lines beneath the cell. Long text output is elided to inline_output_max_lines (default 20) — the full text stays in the preview pane and Output window. DataFrames use a compact, resize-aware column summary; use the preview for horizontal scrolling. Requires cell_frame.

Toggle at runtime with :JovianToggleCellFrame and :JovianToggleMarkdownStyle.


⌨️ Recommended Keybindings

local map = vim.keymap.set

-- Execution
map("n", "<leader>r", "<cmd>JovianRun<CR>", { desc = "Run Cell" })
map("n", "<leader>x", "<cmd>JovianRunAndNext<CR>", { desc = "Run & Next" })
map("n", "<leader>R", "<cmd>JovianRunAll<CR>", { desc = "Run All Cells" })

-- UI
map("n", "<leader>jo", "<cmd>JovianOpen<CR>", { desc = "Open UI" })
map("n", "<leader>jt", "<cmd>JovianToggle<CR>", { desc = "Toggle UI" })
map("n", "<leader>jv", "<cmd>JovianVars<CR>", { desc = "Variables" })
map("n", "<leader>je", "<cmd>JovianEval<CR>", { desc = "Quick eval" })

-- Navigation
map("n", "]c", "<cmd>JovianNextCell<CR>", { desc = "Next Cell" })
map("n", "[c", "<cmd>JovianPrevCell<CR>", { desc = "Prev Cell" })

⚙️ Configuration

Click to expand the full configuration options
require("jovian").setup({
    -- Visuals
    flash_highlight_group = "Visual",
    flash_duration = 300,
    float_border = "rounded", -- single | double | rounded | solid | shadow

    -- Python — nil auto-resolves a usable interpreter. An explicit value is
    -- used verbatim; $JOVIAN_PYTHON wins when no explicit value is set.
    python_interpreter = nil,
    -- Or pin a registered kernelspec. python_interpreter takes precedence.
    kernel_name = nil,

    -- Behavior
    notify_threshold = 10,   -- seconds before a long run notifies
    notify_mode = "all",     -- "all" | "error" | "none"
    show_execution_time = true,
    folding = false,         -- cell-based folds for Python files
    dataframe_page_size = 50,
    remote_cwd = ".",
    default_keymaps = false, -- opt in to jovian's buffer-local defaults

    -- Output (REPL) window: "ondemand" (default) | "always" | "off"
    output_window = "ondemand",

    -- `plt.show()` display: "inline" (Jovian/terminal) or "native" (OS GUI).
    -- :JovianTogglePlot switches modes and restarts the kernel.
    plot_mode = "inline",
    native_plot_backend = "TkAgg", -- e.g. "QtAgg" or "MacOSX"

    -- Opt-in visual layers (all off by default)
    cell_frame = false,
    cell_frame_style = "square", -- "square" | "rounded"
    cell_frame_priority = 100,
    cell_frame_right_pad = 0,      -- reserve N columns to the right of the
                                   -- cell frame for scrollbar plugins
                                   -- (nvim-scrollbar / nvim-scrollview)
    markdown_cell_style = false,
    table_border = "round", -- "round" | "none" | "heavy" | "double"
    math = { enabled = true, position = "center", converter = nil },
    inline_outputs = false,        -- requires cell_frame
    inline_output_max_lines = 20,  -- elide longer inline text output

    -- Inline image placement (terminal cells)
    image_rows = 14,
    image_cols = 56,
    -- Preview-pane image sizing (parses PNG/GIF headers; never upscales)
    preview_cell_pixel_height = 16,
    preview_cell_pixel_aspect = 0.5,
    preview_image_max_cols = nil,  -- nil = fill the preview text area
    preview_image_max_rows = nil,

    -- Default persistent panels. Variables and Output are intentionally not
    -- here: Variables is a float via :JovianVars, Output is governed by
    -- `output_window` above.
    ui = {
        cell_separator_highlight = "text", -- "line" | "text" | "none"
        -- winblend = 0, -- optionally override Neovim's float transparency
        layouts = {
            {
                elements = {
                    { id = "preview", size = 0.70 },
                    { id = "pin", size = 0.30 },
                },
                position = "left", -- "left" | "right" | "top" | "bottom"
                size = 0.35,
            },
        },
    },

    -- Cell status virtual text
    ui_symbols = {
        running = " Running...",
        done = " Done",
        error = " Error",
        interrupted = " Interrupted",
        stale = " Stale",
    },

    -- Magic commands
    suppress_magic_command_errors = true,

    -- Per-group highlight overrides — see Customization below. Keys include
    -- cell_border_code / cell_border_markdown, md_text / md_h1 … md_h6,
    -- md_bold / md_em / md_code / md_bullet / md_quote / md_table_divider /
    -- md_table_header / md_math, and out_divider / out_stdout / out_stderr /
    -- out_result / out_error.
    highlights = {},
})

🧠 How it Works

:JovianRun
  → core.send_cell()
  → jovian-core (Rust) execute_request over the SHELL socket
      ├─ IOPUB stream/status/result/error events
      │    → msgpack-RPC notifications → handlers → UI (inline / preview / pin)
      └─ outputs mirrored to .jovian_cache/<filename>/outputs.json (sidecar)
  • jovian-core (Rust, in core/) spawns and owns the IPython kernel, signs messages with HMAC-SHA256, and speaks the Jupyter v5 wire protocol over pure-Rust ZeroMQ. It exposes a small msgpack-RPC API over stdio.
  • Neovim spawns the binary via vim.uv pipes and exchanges length-prefixed msgpack frames (request / response / notification).
  • Outputs are stored in an nbformat-shaped JSON sidecar at .jovian_cache/<filename>/outputs.json, keyed by cell ID. The inline, preview, and pin renderers all read from this structured store — output is not tied to a markdown file format. Inline DataFrames are compacted to the source window; preview DataFrames are nowrap buffers with horizontal scrolling.
  • Images are transmitted to the terminal via the Kitty graphics protocol (Unicode placeholder mode); Neovim only places the placeholder glyphs.
  • Remote kernels are the same path with ssh standing in for the local python: the remote picks its own free ports and writes the connection file, then a single ssh -L … process both forwards those ports to localhost and runs the kernel — so the ZMQ layer connects to 127.0.0.1 either way.

Why a .py + # %% source format with IDs? Cell IDs stored in the file let the output sidecar correlate with specific cells across edits and sessions, while keeping the source plain, diff-friendly, and source-controllable.


📚 Command Reference

Execution
Command Description
:JovianStart Start / connect a kernel
:JovianRun Run current cell
:JovianRunAndNext Run cell and jump to next
:JovianRunAll Run all cells
:JovianRestartAndRunAll Restart kernel, then run all cells
:JovianRunOnly <tag>... Run cells with any of the given tags
:JovianRunAllExcept <tag>... Run cells whose tags don't intersect the given set
:JovianImport <path.ipynb> One-shot: convert a notebook to .py + sidecar and open it
:JovianExport [path.ipynb] Export the current .py buffer to .ipynb
:JovianRunAbove Run all cells above the cursor
:JovianRunLine Run the current line
:JovianSendSelection Run the visual selection
:JovianRestart Restart the kernel
:JovianInterrupt Interrupt execution
:JovianEval [code] Quick-eval an expression (no history)
:JovianREPL Continuous eval prompt in the live kernel
:JovianPickPython Pick a Python interpreter / kernelspec; restarts kernel

Native .ipynb editing: just :edit foo.ipynb (or nvim foo.ipynb from the shell). The on-disk file stays Jupyter format, but the buffer is the rendered # %% cell view; :w re-serializes back to nbformat. LSP / :JovianRun work exactly as for .py. Set vim.g.jovian_disable_ipynb_open = 1 to opt out.

UI & Layout
Command Description
:JovianOpen / :JovianToggle Open / toggle the panels
:JovianToggleOutput Toggle the Output (REPL) window
:JovianTogglePlot Toggle OS-native Matplotlib windows; restarts the kernel
:JovianToggleVars Toggle the Variables pane
:JovianToggleStatus Toggle cell status virtual text
:JovianToggleCellFrame Toggle bordered cell cards
:JovianToggleInlineOutputs Toggle inline outputs across all python buffers
:JovianToggleCellOutput Collapse / expand the cursor cell's inline output
:JovianToggleMarkdownStyle Toggle markdown cell styling
:JovianPin / :JovianUnpin Pin / unpin current cell output
:JovianTogglePin Toggle the pinned output window
:JovianClearREPL Clear the Output buffer
Cell management
Command Description
:JovianNextCell / :JovianPrevCell Navigate cells
:JovianNewCellBelow / :JovianNewCellAbove Insert a cell
:JovianNewMarkdownCellBelow Insert a markdown cell
:JovianDeleteCell Delete the current cell
:JovianMoveCellUp / :JovianMoveCellDown Reorder cells
:JovianSplitCell Split the cell at the cursor
:JovianMergeBelow / :JovianMergeAbove Merge with next / previous cell
Data & inspection
Command Description
:JovianVars Show variables in a float
:JovianView [var] Inspect a variable / DataFrame
:JovianInspect [expr] Show kernel's docstring for the cursor symbol (Jupyter's ?foo)
Remote / host
Command Description
:JovianConnect Pick an SSH/Tailscale host and activate it
:JovianAddHost / :JovianAddLocal Register a host manually
:JovianUse [name] / :JovianRemoveHost [name] Switch / remove host
:JovianSync [path] rsync local files to the remote host
:JovianTunnelStatus Show the active remote host + kernel state
Cache & diagnostics
Command Description
:JovianClean[!] Clean stale / orphaned cache
:JovianClearCache[!] Clear cell output cache
:JovianClearDiag Clear LSP diagnostics
:JovianDebugImages Probe the Kitty image pipeline
:checkhealth jovian Validate dependencies

🎨 Customization

Override highlight groups to match your theme. Most groups default to a fallback chain that picks up your colorscheme's own heading/accent colors, so they look reasonable out of the box. Override per-group via the highlights table — a string is treated as a link target, a table as nvim_set_hl attributes:

require("jovian").setup({
    highlights = {
        cell_border_code = "Function",            -- link
        cell_border_markdown = { fg = "#e0af68" },  -- attrs
        md_h1 = "@markup.heading.1.markdown",
        out_error = "ErrorMsg",
    },
})
Area Groups
Cell cards JovianCellBorderCode, JovianCellBorderMarkdown
Markdown cells JovianMdH1JovianMdH6, JovianMdBold, JovianMdEm, JovianMdCode, JovianMdBullet, JovianMdQuote, JovianMdTableDivider, JovianMdTableHeader
Inline / preview output JovianOutDivider, JovianOutStdout, JovianOutStderr, JovianOutResult, JovianOutError
Floats & panes JovianFloat, JovianFloatBorder, JovianHeader, JovianSeparator, JovianVariable, JovianType, JovianValue
Cell separator JovianCellMarker

🙏 Acknowledgements

  • jupynvim — the Rust backend architecture (a single core binary speaking the Jupyter wire protocol over msgpack-RPC, with Kitty placeholder image rendering) is modeled directly on jupynvim. Many thanks to its author.
  • vim-jukit — the original inspiration for jovian's cell-based workflow.

About

A Jupyter-like Python development environment for Neovim with seamless local and remote (SSH) execution.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages