vimol 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vimol-0.1.0/LICENSE +20 -0
- vimol-0.1.0/PKG-INFO +302 -0
- vimol-0.1.0/README.md +268 -0
- vimol-0.1.0/pyproject.toml +53 -0
- vimol-0.1.0/setup.cfg +4 -0
- vimol-0.1.0/src/vimol/__init__.py +63 -0
- vimol-0.1.0/src/vimol/__main__.py +4 -0
- vimol-0.1.0/src/vimol/app.py +218 -0
- vimol-0.1.0/src/vimol/arrows.py +91 -0
- vimol-0.1.0/src/vimol/bonds.py +79 -0
- vimol-0.1.0/src/vimol/camera.py +77 -0
- vimol-0.1.0/src/vimol/editor.py +646 -0
- vimol-0.1.0/src/vimol/elements.py +123 -0
- vimol-0.1.0/src/vimol/gl_adapter.py +114 -0
- vimol-0.1.0/src/vimol/gl_render.py +742 -0
- vimol-0.1.0/src/vimol/input.py +252 -0
- vimol-0.1.0/src/vimol/kitty.py +297 -0
- vimol-0.1.0/src/vimol/molecule.py +159 -0
- vimol-0.1.0/src/vimol/parsers/__init__.py +88 -0
- vimol-0.1.0/src/vimol/parsers/mol.py +57 -0
- vimol-0.1.0/src/vimol/parsers/pdb.py +97 -0
- vimol-0.1.0/src/vimol/parsers/xyz.py +68 -0
- vimol-0.1.0/src/vimol/periodic_table.py +97 -0
- vimol-0.1.0/src/vimol/py.typed +0 -0
- vimol-0.1.0/src/vimol/render.py +600 -0
- vimol-0.1.0/src/vimol/scene.py +239 -0
- vimol-0.1.0/src/vimol/templates.py +205 -0
- vimol-0.1.0/src/vimol/viewer.py +1159 -0
- vimol-0.1.0/src/vimol/widget.py +611 -0
- vimol-0.1.0/src/vimol.egg-info/PKG-INFO +302 -0
- vimol-0.1.0/src/vimol.egg-info/SOURCES.txt +36 -0
- vimol-0.1.0/src/vimol.egg-info/dependency_links.txt +1 -0
- vimol-0.1.0/src/vimol.egg-info/entry_points.txt +2 -0
- vimol-0.1.0/src/vimol.egg-info/requires.txt +7 -0
- vimol-0.1.0/src/vimol.egg-info/top_level.txt +1 -0
- vimol-0.1.0/tests/test_editing.py +2392 -0
- vimol-0.1.0/tests/test_gl_render.py +262 -0
- vimol-0.1.0/tests/test_vimol.py +552 -0
vimol-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vimol contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
vimol-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vimol
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A terminal molecular viewer and editor using the Kitty graphics protocol
|
|
5
|
+
Author: vimol contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: molecular,viewer,editor,terminal,kitty,chemistry,visualization,xyz,pdb,sdf,cli,tui
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
23
|
+
Classifier: Topic :: Multimedia :: Graphics :: Viewers
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: numpy>=1.20
|
|
29
|
+
Provides-Extra: gl
|
|
30
|
+
Requires-Dist: moderngl>=5.6; extra == "gl"
|
|
31
|
+
Provides-Extra: test
|
|
32
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# vimol
|
|
36
|
+
|
|
37
|
+
A molecular viewer that runs **in your terminal**. Open an `.xyz`, `.pdb`,
|
|
38
|
+
`.mol`, or `.sdf` file and get a shaded, rotatable 3D structure right where
|
|
39
|
+
you're already working — over SSH, in tmux, next to your editor — without a
|
|
40
|
+
windowing system, a GPU, or a browser.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install vimol
|
|
44
|
+
vimol caffeine.sdf # rotate with the mouse, hover an atom to identify it
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
vimol renders with the
|
|
48
|
+
[Kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/),
|
|
49
|
+
so it needs a terminal that speaks it — **kitty, Ghostty, or WezTerm**. The
|
|
50
|
+
image is drawn entirely in software (pure Python + numpy); OpenGL is an
|
|
51
|
+
optional accelerator, not a requirement.
|
|
52
|
+
|
|
53
|
+
**What you can do with it:**
|
|
54
|
+
|
|
55
|
+
- **Look at a structure** without leaving the terminal — mouse-drag to rotate,
|
|
56
|
+
wheel to zoom, hover to read off an element and coordinates.
|
|
57
|
+
- **Measure** distances, angles, and dihedrals by clicking atoms (`m`).
|
|
58
|
+
- **Edit** a molecule interactively — grow fragments, swap elements, draw
|
|
59
|
+
bonds, delete atoms, and relax clashes — then save back to `.xyz`.
|
|
60
|
+
- **Render stills to PNG** in scripts or on a headless box (no terminal
|
|
61
|
+
needed for `--render`).
|
|
62
|
+
- **Embed the viewer** in your own terminal app — it's a library first, a
|
|
63
|
+
CLI second.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
vimol protein.pdb --render out.png --size 1200x900 # batch still, works headless
|
|
67
|
+
vimol trajectory.xyz --spin --style spacefill # spin a space-filling model
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import vimol
|
|
72
|
+
mol = vimol.load("caffeine.sdf")
|
|
73
|
+
vimol.view(mol, editable=True) # full-screen: mouse rotate/pan/zoom + hover-to-identify
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Why impostor raycasting?
|
|
77
|
+
|
|
78
|
+
To work in *any* Kitty-capable terminal without a GPU, the renderer treats each
|
|
79
|
+
atom as an analytic **sphere** and each bond as an analytic **cylinder**, and
|
|
80
|
+
solves the ray/surface intersection per pixel under an orthographic camera. A
|
|
81
|
+
shared z-buffer resolves occlusion, so spheres and cylinders interpenetrate
|
|
82
|
+
correctly. Everything is vectorized over each primitive's screen-space bounding
|
|
83
|
+
box, which keeps interactive frame-rates for small/medium molecules in pure
|
|
84
|
+
numpy. It is the same "impostor" trick that GPU molecular viewers (PyMOL, VMD,
|
|
85
|
+
NGL) use — just done on the CPU.
|
|
86
|
+
|
|
87
|
+
## Features
|
|
88
|
+
|
|
89
|
+
- **Formats:** XYZ / extended-XYZ (+ trajectories), PDB (ATOM/HETATM/CONECT,
|
|
90
|
+
multi-model), MOL / SDF (V2000, multi-record).
|
|
91
|
+
- **Representations:** ball-and-stick, space-filling (CPK), licorice, wireframe.
|
|
92
|
+
- **Chemistry:** covalent/van-der-Waals radii and CPK colors for the whole
|
|
93
|
+
periodic table; automatic distance-based bond perception (spatial-hash, scales
|
|
94
|
+
to large structures).
|
|
95
|
+
- **Interaction:** mouse-drag rotate, right-drag pan, wheel zoom, arrow/vim keys,
|
|
96
|
+
roll, autospin, live representation switching, trajectory frame stepping.
|
|
97
|
+
- **Pixel-precise mouse:** uses SGR-Pixels (DECSET 1016) when the terminal
|
|
98
|
+
supports it — probed at startup, with automatic fallback to cell coordinates —
|
|
99
|
+
for smooth dragging and accurate **hover-to-identify** atom picking.
|
|
100
|
+
- **Rendering:** Phong lighting (key + fill + specular), depth cueing, and an
|
|
101
|
+
optional **transparent background** — RGBA cutout with premultiplied-alpha
|
|
102
|
+
edge anti-aliasing, so the molecule composites straight onto your terminal
|
|
103
|
+
background (any theme, light or dark). On by default in the interactive viewer.
|
|
104
|
+
- **Interactive by default:** the software renderer drops resolution while you
|
|
105
|
+
drag and supersamples once you stop, so motion stays responsive on the CPU and
|
|
106
|
+
the resting frame is crisp. An optional OpenGL backend (`vimol[gl]`) renders on
|
|
107
|
+
the GPU where one is available.
|
|
108
|
+
- **Output:** live terminal, one-shot Kitty frame to stdout (pipeable), or PNG
|
|
109
|
+
(via a built-in stdlib PNG encoder — no Pillow needed).
|
|
110
|
+
|
|
111
|
+
## Install
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pip install vimol # core viewer + CLI (numpy only)
|
|
115
|
+
pip install "vimol[gl]" # + optional OpenGL backend (adds moderngl)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Requires Python ≥ 3.8; the only runtime dependency is **numpy**. The base
|
|
119
|
+
install does **not** pull in `moderngl` — the GPU backend is opt-in via the
|
|
120
|
+
`[gl]` extra, because it needs a working OpenGL driver that headless servers
|
|
121
|
+
and many clusters lack, and vimol renders fully on the CPU without it. A
|
|
122
|
+
terminal that speaks the Kitty graphics protocol (kitty, Ghostty, WezTerm) is
|
|
123
|
+
needed for *interactive* use; `--render` to PNG works anywhere, including
|
|
124
|
+
headless.
|
|
125
|
+
|
|
126
|
+
From a checkout, for development:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pip install -e ".[test,gl]" # editable install with test + GL extras
|
|
130
|
+
# or run without installing:
|
|
131
|
+
PYTHONPATH=src python -m vimol examples/c60.xyz
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Library usage
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
import vimol
|
|
138
|
+
|
|
139
|
+
mol = vimol.load("caffeine.sdf") # parse (bonds perceived if absent)
|
|
140
|
+
scene = vimol.Scene(mol, 640, 480) # bind camera + renderer, own the size
|
|
141
|
+
scene.camera.orbit(30, -15) # rotate (degrees)
|
|
142
|
+
|
|
143
|
+
img = scene.render() # -> (H, W, 3) uint8 numpy array
|
|
144
|
+
os.write(1, scene.to_kitty()) # ...or paint it in a Kitty terminal
|
|
145
|
+
scene.to_png("out.png") # ...or save a PNG
|
|
146
|
+
|
|
147
|
+
vimol.view(mol) # ...or launch the full interactive widget
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Embedding with your own input loop
|
|
151
|
+
|
|
152
|
+
When you embed the viewer, *your* app owns the terminal and the input loop. Use
|
|
153
|
+
the two building blocks directly and forward only the events you want — so you
|
|
154
|
+
can **intercept the mouse in your own region** and hand it to the molecule:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from vimol import MoleculeWidget, InputDecoder, MouseEvent
|
|
158
|
+
|
|
159
|
+
widget = MoleculeWidget(mol, width_px, height_px) # no terminal, no input loop
|
|
160
|
+
decoder = InputDecoder(pixel=True)
|
|
161
|
+
|
|
162
|
+
# ...in your loop, on bytes you read from the tty:
|
|
163
|
+
for ev in decoder.feed(data):
|
|
164
|
+
if isinstance(ev, MouseEvent) and inside_my_region(ev):
|
|
165
|
+
widget.handle_mouse(ev, origin=(region_x_px, region_y_px)) # -> rotate/pan/zoom/pick
|
|
166
|
+
else:
|
|
167
|
+
my_app_handles(ev) # the event stays yours
|
|
168
|
+
|
|
169
|
+
os.write(1, widget.to_kitty(cols=region_cols, rows=region_rows))
|
|
170
|
+
idx = widget.hovered # atom index under the cursor, or None
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`Viewer` (the full-screen driver) is itself just a thin wrapper over
|
|
174
|
+
`MoleculeWidget` + `InputDecoder`. See `examples/embed_demo.py` for a complete
|
|
175
|
+
host app that puts its own chrome above the molecule and routes the mouse.
|
|
176
|
+
|
|
177
|
+
## Interactive controls
|
|
178
|
+
|
|
179
|
+
| Action | Keys |
|
|
180
|
+
|---|---|
|
|
181
|
+
| Rotate | mouse drag · arrows · `h j k l` |
|
|
182
|
+
| Pan | right-drag · middle-drag |
|
|
183
|
+
| Zoom | wheel · `+` / `-` |
|
|
184
|
+
| Identify atom | hover the cursor over it |
|
|
185
|
+
| Roll | `[` / `]` |
|
|
186
|
+
| Representation | `1` ball · `2` space · `3` licorice · `4` wire · `s` cycle |
|
|
187
|
+
| Autospin | `a` |
|
|
188
|
+
| Measure | `m` toggles; click 2 atoms → distance (Å), 3 → angle, 4 → dihedral; click empty space to reset |
|
|
189
|
+
| Trajectory frame | `n` / `p` |
|
|
190
|
+
| Transparent background | `t` |
|
|
191
|
+
| Depth cue / hi-quality / re-fit / reset | `d` / `g` / `f` / `r` |
|
|
192
|
+
| Help / quit | `?` / `q` or `Esc` |
|
|
193
|
+
|
|
194
|
+
### Editing
|
|
195
|
+
|
|
196
|
+
The `vimol` CLI always opens with editing on. The Python API (`vimol.view()`,
|
|
197
|
+
`Viewer`, `MoleculeWidget`) still defaults to `editable=False` and needs
|
|
198
|
+
`editable=True` to opt in — that way a host application that embeds the
|
|
199
|
+
widget doesn't inherit these keybindings unless it asks for them. When
|
|
200
|
+
editing is on, a few keys change meaning from the classic bindings above
|
|
201
|
+
(`a` = autospin, `s` = cycle style):
|
|
202
|
+
|
|
203
|
+
| Action | Keys |
|
|
204
|
+
|---|---|
|
|
205
|
+
| Append / edit | `a` toggles; then click an H → grow, click a heavy atom → replace it, click empty space → new methane |
|
|
206
|
+
| Delete | `x` toggles; then click an atom to remove it (its terminal hydrogens go with it) |
|
|
207
|
+
| Manual bond | option/alt-drag from one atom to another — draws a bond kept even beyond the auto-perception distance |
|
|
208
|
+
| Cleanup | `c` — relaxes steric clashes and stretched manual bonds |
|
|
209
|
+
| Undo | `u` (reverts the last edit) |
|
|
210
|
+
| Save | `s` (prompts for a filename; asks before overwriting an existing file) |
|
|
211
|
+
| Autospin | `o` (relocated from `a` while editing) |
|
|
212
|
+
|
|
213
|
+
In **append** mode (status bar shows `✎APPEND`) a left *click* — not a drag,
|
|
214
|
+
which still rotates — builds structure:
|
|
215
|
+
|
|
216
|
+
- **click a hydrogen** → grow there: the H is promoted to the element you've
|
|
217
|
+
selected and its freed valences are capped with hydrogens (so an H becomes
|
|
218
|
+
a `–CH3` by default).
|
|
219
|
+
- **click a heavier atom** → *replace* it with the selected element/geometry.
|
|
220
|
+
Terminal hydrogens snap to the new bond length; an under-coordinated atom
|
|
221
|
+
is topped up with hydrogens to meet the chosen valence, and excess bonds
|
|
222
|
+
are left alone (hypervalent atoms are yours to make).
|
|
223
|
+
- **click empty space** → a fresh, fully-capped molecule of the selected
|
|
224
|
+
element is born on the plane through the molecule's center (carbon → methane,
|
|
225
|
+
oxygen → water, etc).
|
|
226
|
+
|
|
227
|
+
The status bar shows what you're placing as two colored, button-styled tokens —
|
|
228
|
+
`adding [ C ] [ tetrahedral ]`. Both are clickable, and each opens a picker
|
|
229
|
+
anchored right above the button you clicked (not centered on screen) so it
|
|
230
|
+
appears where you're already looking; click the same pill again, click
|
|
231
|
+
anywhere outside the panel, or press `Esc`, to dismiss it without changing
|
|
232
|
+
anything — or click the *other* pill to switch straight to that picker.
|
|
233
|
+
|
|
234
|
+
- **Click the element pill** (`[ C ]`) to open a **periodic-table picker**: a
|
|
235
|
+
bordered overlay colored by each element's CPK color. Drive it with the arrow
|
|
236
|
+
keys / `h j k l`, the mouse, or both — click a cell or move to it and press
|
|
237
|
+
Enter. The info line shows the full element name and the geometry it'll build
|
|
238
|
+
with; the lanthanide/actinide gap cells (`**`) jump you down to their own row.
|
|
239
|
+
Choosing a new element resets the geometry to that element's default.
|
|
240
|
+
- **Click the geometry pill** (`[ tetrahedral ]`) to open a **geometry picker**
|
|
241
|
+
listing the hybridization/valence options for the current element — e.g.
|
|
242
|
+
carbon offers `tetrahedral · sp3 · 4 bonds`, `trigonal · sp2 · 3 bonds`, and
|
|
243
|
+
`linear · sp · 2 bonds`; nitrogen and oxygen list their own. The active choice
|
|
244
|
+
is marked `●`. Pick one with the arrows/mouse + Enter, and subsequent builds
|
|
245
|
+
use that geometry (so an sp² carbon grows a `–CH2`/`=CH` centre instead of a
|
|
246
|
+
full tetrahedron).
|
|
247
|
+
|
|
248
|
+
Press `x` to toggle **delete** mode (status bar shows `✗DELETE`; on Kitty-family
|
|
249
|
+
terminals the mouse pointer becomes a **crosshair** while it's armed, and the
|
|
250
|
+
atom under the cursor previews in red). A left click then removes the atom it
|
|
251
|
+
lands on — and, if that atom is a heavy one, its own terminal hydrogens go with
|
|
252
|
+
it, so deleting a methyl carbon takes its three H too. Clicking a hydrogen
|
|
253
|
+
directly removes just that hydrogen; clicking empty space does nothing. Deletion
|
|
254
|
+
is a plain erase — a heavy neighbor that loses a bond is left dangling, with no
|
|
255
|
+
hydrogen added to cap it (unlike replace, which tops up valence). `x` and `a`
|
|
256
|
+
are mutually exclusive: arming one disarms the other.
|
|
257
|
+
|
|
258
|
+
Bond perception is distance-based, so freshly-built atoms can appear "bonded"
|
|
259
|
+
to whatever they merely landed near, and an option-drag manual bond can be
|
|
260
|
+
far longer than a real one. Press `c` to **cleanup**: it relaxes those two
|
|
261
|
+
cases with simple springs — pushing steric-clash bonds apart until the false
|
|
262
|
+
bond disappears, pulling over-long manual bonds toward a real bond length,
|
|
263
|
+
and nudging the angles around the affected atoms back toward their template
|
|
264
|
+
geometry — moving the atoms you just placed far more than the rest of the
|
|
265
|
+
structure. The relaxation animates over roughly half a second, so you watch
|
|
266
|
+
the molecule settle rather than teleport. The status bar hints `⚠ c cleanup`
|
|
267
|
+
whenever there's something for it to fix, and one `u` undoes the whole thing
|
|
268
|
+
like any other edit.
|
|
269
|
+
|
|
270
|
+
Any edit marks the model `[MODIFIED]`; `u` undoes step by step. Press `s` to
|
|
271
|
+
save — the prompt is pre-filled with the source path, saves straight to a new
|
|
272
|
+
file, and asks before replacing an existing one. Pressing `Esc` with unsaved
|
|
273
|
+
changes asks `save before quitting? (y/n/Esc)` instead of quitting silently
|
|
274
|
+
(`q` stays an immediate quit). Geometry for each element comes
|
|
275
|
+
from a small template registry (`templates.py`) keyed by element and valence —
|
|
276
|
+
tetrahedral carbon, pyramidal nitrogen, bent oxygen, … — so new templates are a
|
|
277
|
+
one-line addition.
|
|
278
|
+
|
|
279
|
+
## Project layout
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
src/vimol/
|
|
283
|
+
├── __init__.py # public library API
|
|
284
|
+
├── widget.py # MoleculeWidget: embeddable interaction core (+ picking)
|
|
285
|
+
├── input.py # InputDecoder + MouseEvent/KeyEvent, SGR/pixel mouse
|
|
286
|
+
├── viewer.py # full-screen driver (thin wrapper over widget + input)
|
|
287
|
+
├── app.py # batch/CLI implementation (vimol command)
|
|
288
|
+
├── scene.py # Scene: molecule + camera + renderer + supersampling
|
|
289
|
+
├── render.py # numpy impostor raycaster (spheres + cylinders)
|
|
290
|
+
├── camera.py # orthographic trackball camera
|
|
291
|
+
├── kitty.py # Kitty graphics protocol encoder + terminal geometry
|
|
292
|
+
├── molecule.py # Molecule data model
|
|
293
|
+
├── bonds.py # distance-based bond perception
|
|
294
|
+
├── elements.py # periodic-table radii + CPK colors
|
|
295
|
+
└── parsers/ # xyz / pdb / mol-sdf
|
|
296
|
+
examples/ # water, methane, benzene, C60 + embed/host demos
|
|
297
|
+
tests/ # pytest suite
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## License
|
|
301
|
+
|
|
302
|
+
MIT.
|
vimol-0.1.0/README.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# vimol
|
|
2
|
+
|
|
3
|
+
A molecular viewer that runs **in your terminal**. Open an `.xyz`, `.pdb`,
|
|
4
|
+
`.mol`, or `.sdf` file and get a shaded, rotatable 3D structure right where
|
|
5
|
+
you're already working — over SSH, in tmux, next to your editor — without a
|
|
6
|
+
windowing system, a GPU, or a browser.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install vimol
|
|
10
|
+
vimol caffeine.sdf # rotate with the mouse, hover an atom to identify it
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
vimol renders with the
|
|
14
|
+
[Kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/),
|
|
15
|
+
so it needs a terminal that speaks it — **kitty, Ghostty, or WezTerm**. The
|
|
16
|
+
image is drawn entirely in software (pure Python + numpy); OpenGL is an
|
|
17
|
+
optional accelerator, not a requirement.
|
|
18
|
+
|
|
19
|
+
**What you can do with it:**
|
|
20
|
+
|
|
21
|
+
- **Look at a structure** without leaving the terminal — mouse-drag to rotate,
|
|
22
|
+
wheel to zoom, hover to read off an element and coordinates.
|
|
23
|
+
- **Measure** distances, angles, and dihedrals by clicking atoms (`m`).
|
|
24
|
+
- **Edit** a molecule interactively — grow fragments, swap elements, draw
|
|
25
|
+
bonds, delete atoms, and relax clashes — then save back to `.xyz`.
|
|
26
|
+
- **Render stills to PNG** in scripts or on a headless box (no terminal
|
|
27
|
+
needed for `--render`).
|
|
28
|
+
- **Embed the viewer** in your own terminal app — it's a library first, a
|
|
29
|
+
CLI second.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
vimol protein.pdb --render out.png --size 1200x900 # batch still, works headless
|
|
33
|
+
vimol trajectory.xyz --spin --style spacefill # spin a space-filling model
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import vimol
|
|
38
|
+
mol = vimol.load("caffeine.sdf")
|
|
39
|
+
vimol.view(mol, editable=True) # full-screen: mouse rotate/pan/zoom + hover-to-identify
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Why impostor raycasting?
|
|
43
|
+
|
|
44
|
+
To work in *any* Kitty-capable terminal without a GPU, the renderer treats each
|
|
45
|
+
atom as an analytic **sphere** and each bond as an analytic **cylinder**, and
|
|
46
|
+
solves the ray/surface intersection per pixel under an orthographic camera. A
|
|
47
|
+
shared z-buffer resolves occlusion, so spheres and cylinders interpenetrate
|
|
48
|
+
correctly. Everything is vectorized over each primitive's screen-space bounding
|
|
49
|
+
box, which keeps interactive frame-rates for small/medium molecules in pure
|
|
50
|
+
numpy. It is the same "impostor" trick that GPU molecular viewers (PyMOL, VMD,
|
|
51
|
+
NGL) use — just done on the CPU.
|
|
52
|
+
|
|
53
|
+
## Features
|
|
54
|
+
|
|
55
|
+
- **Formats:** XYZ / extended-XYZ (+ trajectories), PDB (ATOM/HETATM/CONECT,
|
|
56
|
+
multi-model), MOL / SDF (V2000, multi-record).
|
|
57
|
+
- **Representations:** ball-and-stick, space-filling (CPK), licorice, wireframe.
|
|
58
|
+
- **Chemistry:** covalent/van-der-Waals radii and CPK colors for the whole
|
|
59
|
+
periodic table; automatic distance-based bond perception (spatial-hash, scales
|
|
60
|
+
to large structures).
|
|
61
|
+
- **Interaction:** mouse-drag rotate, right-drag pan, wheel zoom, arrow/vim keys,
|
|
62
|
+
roll, autospin, live representation switching, trajectory frame stepping.
|
|
63
|
+
- **Pixel-precise mouse:** uses SGR-Pixels (DECSET 1016) when the terminal
|
|
64
|
+
supports it — probed at startup, with automatic fallback to cell coordinates —
|
|
65
|
+
for smooth dragging and accurate **hover-to-identify** atom picking.
|
|
66
|
+
- **Rendering:** Phong lighting (key + fill + specular), depth cueing, and an
|
|
67
|
+
optional **transparent background** — RGBA cutout with premultiplied-alpha
|
|
68
|
+
edge anti-aliasing, so the molecule composites straight onto your terminal
|
|
69
|
+
background (any theme, light or dark). On by default in the interactive viewer.
|
|
70
|
+
- **Interactive by default:** the software renderer drops resolution while you
|
|
71
|
+
drag and supersamples once you stop, so motion stays responsive on the CPU and
|
|
72
|
+
the resting frame is crisp. An optional OpenGL backend (`vimol[gl]`) renders on
|
|
73
|
+
the GPU where one is available.
|
|
74
|
+
- **Output:** live terminal, one-shot Kitty frame to stdout (pipeable), or PNG
|
|
75
|
+
(via a built-in stdlib PNG encoder — no Pillow needed).
|
|
76
|
+
|
|
77
|
+
## Install
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install vimol # core viewer + CLI (numpy only)
|
|
81
|
+
pip install "vimol[gl]" # + optional OpenGL backend (adds moderngl)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Requires Python ≥ 3.8; the only runtime dependency is **numpy**. The base
|
|
85
|
+
install does **not** pull in `moderngl` — the GPU backend is opt-in via the
|
|
86
|
+
`[gl]` extra, because it needs a working OpenGL driver that headless servers
|
|
87
|
+
and many clusters lack, and vimol renders fully on the CPU without it. A
|
|
88
|
+
terminal that speaks the Kitty graphics protocol (kitty, Ghostty, WezTerm) is
|
|
89
|
+
needed for *interactive* use; `--render` to PNG works anywhere, including
|
|
90
|
+
headless.
|
|
91
|
+
|
|
92
|
+
From a checkout, for development:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install -e ".[test,gl]" # editable install with test + GL extras
|
|
96
|
+
# or run without installing:
|
|
97
|
+
PYTHONPATH=src python -m vimol examples/c60.xyz
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Library usage
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
import vimol
|
|
104
|
+
|
|
105
|
+
mol = vimol.load("caffeine.sdf") # parse (bonds perceived if absent)
|
|
106
|
+
scene = vimol.Scene(mol, 640, 480) # bind camera + renderer, own the size
|
|
107
|
+
scene.camera.orbit(30, -15) # rotate (degrees)
|
|
108
|
+
|
|
109
|
+
img = scene.render() # -> (H, W, 3) uint8 numpy array
|
|
110
|
+
os.write(1, scene.to_kitty()) # ...or paint it in a Kitty terminal
|
|
111
|
+
scene.to_png("out.png") # ...or save a PNG
|
|
112
|
+
|
|
113
|
+
vimol.view(mol) # ...or launch the full interactive widget
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Embedding with your own input loop
|
|
117
|
+
|
|
118
|
+
When you embed the viewer, *your* app owns the terminal and the input loop. Use
|
|
119
|
+
the two building blocks directly and forward only the events you want — so you
|
|
120
|
+
can **intercept the mouse in your own region** and hand it to the molecule:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from vimol import MoleculeWidget, InputDecoder, MouseEvent
|
|
124
|
+
|
|
125
|
+
widget = MoleculeWidget(mol, width_px, height_px) # no terminal, no input loop
|
|
126
|
+
decoder = InputDecoder(pixel=True)
|
|
127
|
+
|
|
128
|
+
# ...in your loop, on bytes you read from the tty:
|
|
129
|
+
for ev in decoder.feed(data):
|
|
130
|
+
if isinstance(ev, MouseEvent) and inside_my_region(ev):
|
|
131
|
+
widget.handle_mouse(ev, origin=(region_x_px, region_y_px)) # -> rotate/pan/zoom/pick
|
|
132
|
+
else:
|
|
133
|
+
my_app_handles(ev) # the event stays yours
|
|
134
|
+
|
|
135
|
+
os.write(1, widget.to_kitty(cols=region_cols, rows=region_rows))
|
|
136
|
+
idx = widget.hovered # atom index under the cursor, or None
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`Viewer` (the full-screen driver) is itself just a thin wrapper over
|
|
140
|
+
`MoleculeWidget` + `InputDecoder`. See `examples/embed_demo.py` for a complete
|
|
141
|
+
host app that puts its own chrome above the molecule and routes the mouse.
|
|
142
|
+
|
|
143
|
+
## Interactive controls
|
|
144
|
+
|
|
145
|
+
| Action | Keys |
|
|
146
|
+
|---|---|
|
|
147
|
+
| Rotate | mouse drag · arrows · `h j k l` |
|
|
148
|
+
| Pan | right-drag · middle-drag |
|
|
149
|
+
| Zoom | wheel · `+` / `-` |
|
|
150
|
+
| Identify atom | hover the cursor over it |
|
|
151
|
+
| Roll | `[` / `]` |
|
|
152
|
+
| Representation | `1` ball · `2` space · `3` licorice · `4` wire · `s` cycle |
|
|
153
|
+
| Autospin | `a` |
|
|
154
|
+
| Measure | `m` toggles; click 2 atoms → distance (Å), 3 → angle, 4 → dihedral; click empty space to reset |
|
|
155
|
+
| Trajectory frame | `n` / `p` |
|
|
156
|
+
| Transparent background | `t` |
|
|
157
|
+
| Depth cue / hi-quality / re-fit / reset | `d` / `g` / `f` / `r` |
|
|
158
|
+
| Help / quit | `?` / `q` or `Esc` |
|
|
159
|
+
|
|
160
|
+
### Editing
|
|
161
|
+
|
|
162
|
+
The `vimol` CLI always opens with editing on. The Python API (`vimol.view()`,
|
|
163
|
+
`Viewer`, `MoleculeWidget`) still defaults to `editable=False` and needs
|
|
164
|
+
`editable=True` to opt in — that way a host application that embeds the
|
|
165
|
+
widget doesn't inherit these keybindings unless it asks for them. When
|
|
166
|
+
editing is on, a few keys change meaning from the classic bindings above
|
|
167
|
+
(`a` = autospin, `s` = cycle style):
|
|
168
|
+
|
|
169
|
+
| Action | Keys |
|
|
170
|
+
|---|---|
|
|
171
|
+
| Append / edit | `a` toggles; then click an H → grow, click a heavy atom → replace it, click empty space → new methane |
|
|
172
|
+
| Delete | `x` toggles; then click an atom to remove it (its terminal hydrogens go with it) |
|
|
173
|
+
| Manual bond | option/alt-drag from one atom to another — draws a bond kept even beyond the auto-perception distance |
|
|
174
|
+
| Cleanup | `c` — relaxes steric clashes and stretched manual bonds |
|
|
175
|
+
| Undo | `u` (reverts the last edit) |
|
|
176
|
+
| Save | `s` (prompts for a filename; asks before overwriting an existing file) |
|
|
177
|
+
| Autospin | `o` (relocated from `a` while editing) |
|
|
178
|
+
|
|
179
|
+
In **append** mode (status bar shows `✎APPEND`) a left *click* — not a drag,
|
|
180
|
+
which still rotates — builds structure:
|
|
181
|
+
|
|
182
|
+
- **click a hydrogen** → grow there: the H is promoted to the element you've
|
|
183
|
+
selected and its freed valences are capped with hydrogens (so an H becomes
|
|
184
|
+
a `–CH3` by default).
|
|
185
|
+
- **click a heavier atom** → *replace* it with the selected element/geometry.
|
|
186
|
+
Terminal hydrogens snap to the new bond length; an under-coordinated atom
|
|
187
|
+
is topped up with hydrogens to meet the chosen valence, and excess bonds
|
|
188
|
+
are left alone (hypervalent atoms are yours to make).
|
|
189
|
+
- **click empty space** → a fresh, fully-capped molecule of the selected
|
|
190
|
+
element is born on the plane through the molecule's center (carbon → methane,
|
|
191
|
+
oxygen → water, etc).
|
|
192
|
+
|
|
193
|
+
The status bar shows what you're placing as two colored, button-styled tokens —
|
|
194
|
+
`adding [ C ] [ tetrahedral ]`. Both are clickable, and each opens a picker
|
|
195
|
+
anchored right above the button you clicked (not centered on screen) so it
|
|
196
|
+
appears where you're already looking; click the same pill again, click
|
|
197
|
+
anywhere outside the panel, or press `Esc`, to dismiss it without changing
|
|
198
|
+
anything — or click the *other* pill to switch straight to that picker.
|
|
199
|
+
|
|
200
|
+
- **Click the element pill** (`[ C ]`) to open a **periodic-table picker**: a
|
|
201
|
+
bordered overlay colored by each element's CPK color. Drive it with the arrow
|
|
202
|
+
keys / `h j k l`, the mouse, or both — click a cell or move to it and press
|
|
203
|
+
Enter. The info line shows the full element name and the geometry it'll build
|
|
204
|
+
with; the lanthanide/actinide gap cells (`**`) jump you down to their own row.
|
|
205
|
+
Choosing a new element resets the geometry to that element's default.
|
|
206
|
+
- **Click the geometry pill** (`[ tetrahedral ]`) to open a **geometry picker**
|
|
207
|
+
listing the hybridization/valence options for the current element — e.g.
|
|
208
|
+
carbon offers `tetrahedral · sp3 · 4 bonds`, `trigonal · sp2 · 3 bonds`, and
|
|
209
|
+
`linear · sp · 2 bonds`; nitrogen and oxygen list their own. The active choice
|
|
210
|
+
is marked `●`. Pick one with the arrows/mouse + Enter, and subsequent builds
|
|
211
|
+
use that geometry (so an sp² carbon grows a `–CH2`/`=CH` centre instead of a
|
|
212
|
+
full tetrahedron).
|
|
213
|
+
|
|
214
|
+
Press `x` to toggle **delete** mode (status bar shows `✗DELETE`; on Kitty-family
|
|
215
|
+
terminals the mouse pointer becomes a **crosshair** while it's armed, and the
|
|
216
|
+
atom under the cursor previews in red). A left click then removes the atom it
|
|
217
|
+
lands on — and, if that atom is a heavy one, its own terminal hydrogens go with
|
|
218
|
+
it, so deleting a methyl carbon takes its three H too. Clicking a hydrogen
|
|
219
|
+
directly removes just that hydrogen; clicking empty space does nothing. Deletion
|
|
220
|
+
is a plain erase — a heavy neighbor that loses a bond is left dangling, with no
|
|
221
|
+
hydrogen added to cap it (unlike replace, which tops up valence). `x` and `a`
|
|
222
|
+
are mutually exclusive: arming one disarms the other.
|
|
223
|
+
|
|
224
|
+
Bond perception is distance-based, so freshly-built atoms can appear "bonded"
|
|
225
|
+
to whatever they merely landed near, and an option-drag manual bond can be
|
|
226
|
+
far longer than a real one. Press `c` to **cleanup**: it relaxes those two
|
|
227
|
+
cases with simple springs — pushing steric-clash bonds apart until the false
|
|
228
|
+
bond disappears, pulling over-long manual bonds toward a real bond length,
|
|
229
|
+
and nudging the angles around the affected atoms back toward their template
|
|
230
|
+
geometry — moving the atoms you just placed far more than the rest of the
|
|
231
|
+
structure. The relaxation animates over roughly half a second, so you watch
|
|
232
|
+
the molecule settle rather than teleport. The status bar hints `⚠ c cleanup`
|
|
233
|
+
whenever there's something for it to fix, and one `u` undoes the whole thing
|
|
234
|
+
like any other edit.
|
|
235
|
+
|
|
236
|
+
Any edit marks the model `[MODIFIED]`; `u` undoes step by step. Press `s` to
|
|
237
|
+
save — the prompt is pre-filled with the source path, saves straight to a new
|
|
238
|
+
file, and asks before replacing an existing one. Pressing `Esc` with unsaved
|
|
239
|
+
changes asks `save before quitting? (y/n/Esc)` instead of quitting silently
|
|
240
|
+
(`q` stays an immediate quit). Geometry for each element comes
|
|
241
|
+
from a small template registry (`templates.py`) keyed by element and valence —
|
|
242
|
+
tetrahedral carbon, pyramidal nitrogen, bent oxygen, … — so new templates are a
|
|
243
|
+
one-line addition.
|
|
244
|
+
|
|
245
|
+
## Project layout
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
src/vimol/
|
|
249
|
+
├── __init__.py # public library API
|
|
250
|
+
├── widget.py # MoleculeWidget: embeddable interaction core (+ picking)
|
|
251
|
+
├── input.py # InputDecoder + MouseEvent/KeyEvent, SGR/pixel mouse
|
|
252
|
+
├── viewer.py # full-screen driver (thin wrapper over widget + input)
|
|
253
|
+
├── app.py # batch/CLI implementation (vimol command)
|
|
254
|
+
├── scene.py # Scene: molecule + camera + renderer + supersampling
|
|
255
|
+
├── render.py # numpy impostor raycaster (spheres + cylinders)
|
|
256
|
+
├── camera.py # orthographic trackball camera
|
|
257
|
+
├── kitty.py # Kitty graphics protocol encoder + terminal geometry
|
|
258
|
+
├── molecule.py # Molecule data model
|
|
259
|
+
├── bonds.py # distance-based bond perception
|
|
260
|
+
├── elements.py # periodic-table radii + CPK colors
|
|
261
|
+
└── parsers/ # xyz / pdb / mol-sdf
|
|
262
|
+
examples/ # water, methane, benzene, C60 + embed/host demos
|
|
263
|
+
tests/ # pytest suite
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vimol"
|
|
7
|
+
dynamic = ["version"] # single source of truth: vimol.__version__ (see [tool.setuptools.dynamic])
|
|
8
|
+
description = "A terminal molecular viewer and editor using the Kitty graphics protocol"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "vimol contributors" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"molecular", "viewer", "editor", "terminal", "kitty", "chemistry",
|
|
16
|
+
"visualization", "xyz", "pdb", "sdf", "cli", "tui",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Environment :: Console",
|
|
21
|
+
"Intended Audience :: Science/Research",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
26
|
+
"Programming Language :: Python :: 3.8",
|
|
27
|
+
"Programming Language :: Python :: 3.9",
|
|
28
|
+
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Programming Language :: Python :: 3.13",
|
|
32
|
+
"Topic :: Scientific/Engineering :: Chemistry",
|
|
33
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
34
|
+
"Topic :: Multimedia :: Graphics :: Viewers",
|
|
35
|
+
"Typing :: Typed",
|
|
36
|
+
]
|
|
37
|
+
dependencies = ["numpy>=1.20"]
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
gl = ["moderngl>=5.6"]
|
|
41
|
+
test = ["pytest>=7"]
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
vimol = "vimol.app:main"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.dynamic]
|
|
47
|
+
version = { attr = "vimol.__version__" }
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["src"]
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.package-data]
|
|
53
|
+
vimol = ["py.typed"]
|
vimol-0.1.0/setup.cfg
ADDED