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.
Files changed (38) hide show
  1. vimol-0.1.0/LICENSE +20 -0
  2. vimol-0.1.0/PKG-INFO +302 -0
  3. vimol-0.1.0/README.md +268 -0
  4. vimol-0.1.0/pyproject.toml +53 -0
  5. vimol-0.1.0/setup.cfg +4 -0
  6. vimol-0.1.0/src/vimol/__init__.py +63 -0
  7. vimol-0.1.0/src/vimol/__main__.py +4 -0
  8. vimol-0.1.0/src/vimol/app.py +218 -0
  9. vimol-0.1.0/src/vimol/arrows.py +91 -0
  10. vimol-0.1.0/src/vimol/bonds.py +79 -0
  11. vimol-0.1.0/src/vimol/camera.py +77 -0
  12. vimol-0.1.0/src/vimol/editor.py +646 -0
  13. vimol-0.1.0/src/vimol/elements.py +123 -0
  14. vimol-0.1.0/src/vimol/gl_adapter.py +114 -0
  15. vimol-0.1.0/src/vimol/gl_render.py +742 -0
  16. vimol-0.1.0/src/vimol/input.py +252 -0
  17. vimol-0.1.0/src/vimol/kitty.py +297 -0
  18. vimol-0.1.0/src/vimol/molecule.py +159 -0
  19. vimol-0.1.0/src/vimol/parsers/__init__.py +88 -0
  20. vimol-0.1.0/src/vimol/parsers/mol.py +57 -0
  21. vimol-0.1.0/src/vimol/parsers/pdb.py +97 -0
  22. vimol-0.1.0/src/vimol/parsers/xyz.py +68 -0
  23. vimol-0.1.0/src/vimol/periodic_table.py +97 -0
  24. vimol-0.1.0/src/vimol/py.typed +0 -0
  25. vimol-0.1.0/src/vimol/render.py +600 -0
  26. vimol-0.1.0/src/vimol/scene.py +239 -0
  27. vimol-0.1.0/src/vimol/templates.py +205 -0
  28. vimol-0.1.0/src/vimol/viewer.py +1159 -0
  29. vimol-0.1.0/src/vimol/widget.py +611 -0
  30. vimol-0.1.0/src/vimol.egg-info/PKG-INFO +302 -0
  31. vimol-0.1.0/src/vimol.egg-info/SOURCES.txt +36 -0
  32. vimol-0.1.0/src/vimol.egg-info/dependency_links.txt +1 -0
  33. vimol-0.1.0/src/vimol.egg-info/entry_points.txt +2 -0
  34. vimol-0.1.0/src/vimol.egg-info/requires.txt +7 -0
  35. vimol-0.1.0/src/vimol.egg-info/top_level.txt +1 -0
  36. vimol-0.1.0/tests/test_editing.py +2392 -0
  37. vimol-0.1.0/tests/test_gl_render.py +262 -0
  38. 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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+