Skip to main content

A terminal molecular viewer and editor using the Kitty graphics protocol

Project description

vimol

A molecular viewer that runs in your terminal. Open an .xyz, .pdb, .mol, or .sdf file and get a shaded, rotatable 3D structure right where you're already working — over SSH, in tmux, next to your editor — without a windowing system, a GPU, or a browser.

pip install vimol
vimol caffeine.sdf          # rotate with the mouse, hover an atom to identify it

vimol renders with the Kitty graphics protocol, so it needs a terminal that speaks it — kitty, Ghostty, or WezTerm. The image is drawn entirely in software (pure Python + numpy); OpenGL is an optional accelerator, not a requirement.

What you can do with it:

  • Look at a structure without leaving the terminal — mouse-drag to rotate, wheel to zoom, hover to read off an element and coordinates.
  • Measure distances, angles, and dihedrals by clicking atoms (m).
  • Edit a molecule interactively — grow fragments, swap elements, draw bonds, delete atoms, and relax clashes — then save back to .xyz.
  • Render stills to PNG in scripts or on a headless box (no terminal needed for --render).
  • Embed the viewer in your own terminal app — it's a library first, a CLI second.
vimol protein.pdb --render out.png --size 1200x900   # batch still, works headless
vimol trajectory.xyz --spin --style spacefill        # spin a space-filling model
import vimol
mol = vimol.load("caffeine.sdf")
vimol.view(mol, editable=True)   # full-screen: mouse rotate/pan/zoom + hover-to-identify

Why impostor raycasting?

To work in any Kitty-capable terminal without a GPU, the renderer treats each atom as an analytic sphere and each bond as an analytic cylinder, and solves the ray/surface intersection per pixel under an orthographic camera. A shared z-buffer resolves occlusion, so spheres and cylinders interpenetrate correctly. Everything is vectorized over each primitive's screen-space bounding box, which keeps interactive frame-rates for small/medium molecules in pure numpy. It is the same "impostor" trick that GPU molecular viewers (PyMOL, VMD, NGL) use — just done on the CPU.

Features

  • Formats: XYZ / extended-XYZ (+ trajectories), PDB (ATOM/HETATM/CONECT, multi-model), MOL / SDF (V2000, multi-record).
  • Representations: ball-and-stick, space-filling (CPK), licorice, wireframe.
  • Chemistry: covalent/van-der-Waals radii and CPK colors for the whole periodic table; automatic distance-based bond perception (spatial-hash, scales to large structures).
  • Interaction: mouse-drag rotate, right-drag pan, wheel zoom, arrow/vim keys, roll, autospin, live representation switching, trajectory frame stepping.
  • Pixel-precise mouse: uses SGR-Pixels (DECSET 1016) when the terminal supports it — probed at startup, with automatic fallback to cell coordinates — for smooth dragging and accurate hover-to-identify atom picking.
  • Rendering: Phong lighting (key + fill + specular), depth cueing, and an optional transparent background — RGBA cutout with premultiplied-alpha edge anti-aliasing, so the molecule composites straight onto your terminal background (any theme, light or dark). On by default in the interactive viewer.
  • Interactive by default: the software renderer drops resolution while you drag and supersamples once you stop, so motion stays responsive on the CPU and the resting frame is crisp. An optional OpenGL backend (vimol[gl]) renders on the GPU where one is available.
  • Output: live terminal, one-shot Kitty frame to stdout (pipeable), or PNG (via a built-in stdlib PNG encoder — no Pillow needed).

Install

pip install vimol            # core viewer + CLI (numpy only)
pip install "vimol[gl]"      # + optional OpenGL backend (adds moderngl)

Requires Python ≥ 3.8; the only runtime dependency is numpy. The base install does not pull in moderngl — the GPU backend is opt-in via the [gl] extra, because it needs a working OpenGL driver that headless servers and many clusters lack, and vimol renders fully on the CPU without it. A terminal that speaks the Kitty graphics protocol (kitty, Ghostty, WezTerm) is needed for interactive use; --render to PNG works anywhere, including headless.

From a checkout, for development:

pip install -e ".[test,gl]"      # editable install with test + GL extras
# or run without installing:
PYTHONPATH=src python -m vimol examples/c60.xyz

Library usage

import vimol

mol = vimol.load("caffeine.sdf")     # parse (bonds perceived if absent)
scene = vimol.Scene(mol, 640, 480)   # bind camera + renderer, own the size
scene.camera.orbit(30, -15)            # rotate (degrees)

img = scene.render()                   # -> (H, W, 3) uint8 numpy array
os.write(1, scene.to_kitty())          # ...or paint it in a Kitty terminal
scene.to_png("out.png")                # ...or save a PNG

vimol.view(mol)                      # ...or launch the full interactive widget

Embedding with your own input loop

When you embed the viewer, your app owns the terminal and the input loop. Use the two building blocks directly and forward only the events you want — so you can intercept the mouse in your own region and hand it to the molecule:

from vimol import MoleculeWidget, InputDecoder, MouseEvent

widget = MoleculeWidget(mol, width_px, height_px)   # no terminal, no input loop
decoder = InputDecoder(pixel=True)

# ...in your loop, on bytes you read from the tty:
for ev in decoder.feed(data):
    if isinstance(ev, MouseEvent) and inside_my_region(ev):
        widget.handle_mouse(ev, origin=(region_x_px, region_y_px))  # -> rotate/pan/zoom/pick
    else:
        my_app_handles(ev)                          # the event stays yours

os.write(1, widget.to_kitty(cols=region_cols, rows=region_rows))
idx = widget.hovered   # atom index under the cursor, or None

Viewer (the full-screen driver) is itself just a thin wrapper over MoleculeWidget + InputDecoder. See examples/embed_demo.py for a complete host app that puts its own chrome above the molecule and routes the mouse.

Interactive controls

Action Keys
Rotate mouse drag · arrows · h j k l
Pan right-drag · middle-drag
Zoom wheel · + / -
Identify atom hover the cursor over it
Roll [ / ]
Representation 1 ball · 2 space · 3 licorice · 4 wire · s cycle
Autospin a
Measure m toggles; click 2 atoms → distance (Å), 3 → angle, 4 → dihedral; click empty space to reset
Trajectory frame n / p
Transparent background t
Depth cue / hi-quality / re-fit / reset d / g / f / r
Help / quit ? / q or Esc

Editing

The vimol CLI always opens with editing on. The Python API (vimol.view(), Viewer, MoleculeWidget) still defaults to editable=False and needs editable=True to opt in — that way a host application that embeds the widget doesn't inherit these keybindings unless it asks for them. When editing is on, a few keys change meaning from the classic bindings above (a = autospin, s = cycle style):

Action Keys
Append / edit a toggles; then click an H → grow, click a heavy atom → replace it, click empty space → new methane
Delete x toggles; then click an atom to remove it (its terminal hydrogens go with it)
Manual bond option/alt-drag from one atom to another — draws a bond kept even beyond the auto-perception distance
Cleanup c — relaxes steric clashes and stretched manual bonds
Undo u (reverts the last edit)
Save s (prompts for a filename; asks before overwriting an existing file)
Autospin o (relocated from a while editing)

In append mode (status bar shows ✎APPEND) a left click — not a drag, which still rotates — builds structure:

  • click a hydrogen → grow there: the H is promoted to the element you've selected and its freed valences are capped with hydrogens (so an H becomes a –CH3 by default).
  • click a heavier atomreplace it with the selected element/geometry. Terminal hydrogens snap to the new bond length; an under-coordinated atom is topped up with hydrogens to meet the chosen valence, and excess bonds are left alone (hypervalent atoms are yours to make).
  • click empty space → a fresh, fully-capped molecule of the selected element is born on the plane through the molecule's center (carbon → methane, oxygen → water, etc).

The status bar shows what you're placing as two colored, button-styled tokens — adding [ C ] [ tetrahedral ]. Both are clickable, and each opens a picker anchored right above the button you clicked (not centered on screen) so it appears where you're already looking; click the same pill again, click anywhere outside the panel, or press Esc, to dismiss it without changing anything — or click the other pill to switch straight to that picker.

  • Click the element pill ([ C ]) to open a periodic-table picker: a bordered overlay colored by each element's CPK color. Drive it with the arrow keys / h j k l, the mouse, or both — click a cell or move to it and press Enter. The info line shows the full element name and the geometry it'll build with; the lanthanide/actinide gap cells (**) jump you down to their own row. Choosing a new element resets the geometry to that element's default.
  • Click the geometry pill ([ tetrahedral ]) to open a geometry picker listing the hybridization/valence options for the current element — e.g. carbon offers tetrahedral · sp3 · 4 bonds, trigonal · sp2 · 3 bonds, and linear · sp · 2 bonds; nitrogen and oxygen list their own. The active choice is marked . Pick one with the arrows/mouse + Enter, and subsequent builds use that geometry (so an sp² carbon grows a –CH2/=CH centre instead of a full tetrahedron).

Press x to toggle delete mode (status bar shows ✗DELETE; on Kitty-family terminals the mouse pointer becomes a crosshair while it's armed, and the atom under the cursor previews in red). A left click then removes the atom it lands on — and, if that atom is a heavy one, its own terminal hydrogens go with it, so deleting a methyl carbon takes its three H too. Clicking a hydrogen directly removes just that hydrogen; clicking empty space does nothing. Deletion is a plain erase — a heavy neighbor that loses a bond is left dangling, with no hydrogen added to cap it (unlike replace, which tops up valence). x and a are mutually exclusive: arming one disarms the other.

Bond perception is distance-based, so freshly-built atoms can appear "bonded" to whatever they merely landed near, and an option-drag manual bond can be far longer than a real one. Press c to cleanup: it relaxes those two cases with simple springs — pushing steric-clash bonds apart until the false bond disappears, pulling over-long manual bonds toward a real bond length, and nudging the angles around the affected atoms back toward their template geometry — moving the atoms you just placed far more than the rest of the structure. The relaxation animates over roughly half a second, so you watch the molecule settle rather than teleport. The status bar hints ⚠ c cleanup whenever there's something for it to fix, and one u undoes the whole thing like any other edit.

Any edit marks the model [MODIFIED]; u undoes step by step. Press s to save — the prompt is pre-filled with the source path, saves straight to a new file, and asks before replacing an existing one. Pressing Esc with unsaved changes asks save before quitting? (y/n/Esc) instead of quitting silently (q stays an immediate quit). Geometry for each element comes from a small template registry (templates.py) keyed by element and valence — tetrahedral carbon, pyramidal nitrogen, bent oxygen, … — so new templates are a one-line addition.

Project layout

src/vimol/
├── __init__.py             # public library API
├── widget.py               # MoleculeWidget: embeddable interaction core (+ picking)
├── input.py                # InputDecoder + MouseEvent/KeyEvent, SGR/pixel mouse
├── viewer.py               # full-screen driver (thin wrapper over widget + input)
├── app.py                  # batch/CLI implementation (vimol command)
├── scene.py                # Scene: molecule + camera + renderer + supersampling
├── render.py               # numpy impostor raycaster (spheres + cylinders)
├── camera.py               # orthographic trackball camera
├── kitty.py                # Kitty graphics protocol encoder + terminal geometry
├── molecule.py             # Molecule data model
├── bonds.py                # distance-based bond perception
├── elements.py             # periodic-table radii + CPK colors
└── parsers/                # xyz / pdb / mol-sdf
examples/                   # water, methane, benzene, C60 + embed/host demos
tests/                      # pytest suite

License

MIT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

vimol-0.1.0.tar.gz (122.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vimol-0.1.0-py3-none-any.whl (96.3 kB view details)

Uploaded Python 3

File details

Details for the file vimol-0.1.0.tar.gz.

File metadata

  • Download URL: vimol-0.1.0.tar.gz
  • Upload date:
  • Size: 122.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for vimol-0.1.0.tar.gz
Algorithm Hash digest
SHA256 fcb49abe6c9bb88c477f12a696d39458a32116105d3bd17c6477a40eca1d28b7
MD5 700436d5ad7586b4e69f13c3553d437a
BLAKE2b-256 6d96150c7921c80c1e3693d18cea76050bc6576b09350929a4b0202897e3f96e

See more details on using hashes here.

File details

Details for the file vimol-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: vimol-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 96.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for vimol-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a1d35a3c0a1c8adcdaae211bf2978548336975a37d95496ffcdd6736733a6833
MD5 52c9bb7fca58a4ccae5dafd2c840a480
BLAKE2b-256 a2e8ea3c7b5b7c99aaa864c54de8e25fc09115bafe4cd8674cb8acd9e0e6ae8f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page