specterm1d 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 (93) hide show
  1. specterm1d-0.1.0/.github/workflows/ci.yml +67 -0
  2. specterm1d-0.1.0/.github/workflows/release.yml +63 -0
  3. specterm1d-0.1.0/.gitignore +13 -0
  4. specterm1d-0.1.0/.readthedocs.yaml +23 -0
  5. specterm1d-0.1.0/AGENTS.md +43 -0
  6. specterm1d-0.1.0/LICENSE +29 -0
  7. specterm1d-0.1.0/PKG-INFO +276 -0
  8. specterm1d-0.1.0/README.md +234 -0
  9. specterm1d-0.1.0/claude_docs/plans/2026-08-24-specterm1d.md +8436 -0
  10. specterm1d-0.1.0/claude_docs/plans/2026-08-25-specterm1d-gui-renderer.md +2279 -0
  11. specterm1d-0.1.0/claude_docs/plans/2026-08-31-documentation-and-ci.md +651 -0
  12. specterm1d-0.1.0/claude_docs/specs/2026-08-24-specterm1d-design.md +453 -0
  13. specterm1d-0.1.0/claude_docs/specs/2026-08-25-specterm1d-gui-renderer-design.md +455 -0
  14. specterm1d-0.1.0/claude_docs/specs/2026-08-31-documentation-and-ci-design.md +146 -0
  15. specterm1d-0.1.0/docs/batch.md +6 -0
  16. specterm1d-0.1.0/docs/conf.py +23 -0
  17. specterm1d-0.1.0/docs/differences.md +15 -0
  18. specterm1d-0.1.0/docs/index.md +16 -0
  19. specterm1d-0.1.0/docs/install.md +6 -0
  20. specterm1d-0.1.0/docs/keys.md +166 -0
  21. specterm1d-0.1.0/docs/measuring.md +6 -0
  22. specterm1d-0.1.0/docs/quickstart.md +6 -0
  23. specterm1d-0.1.0/docs/terminals.md +28 -0
  24. specterm1d-0.1.0/pyproject.toml +82 -0
  25. specterm1d-0.1.0/setup.cfg +4 -0
  26. specterm1d-0.1.0/specterm1d/__init__.py +0 -0
  27. specterm1d-0.1.0/specterm1d/cli.py +155 -0
  28. specterm1d-0.1.0/specterm1d/commands/__init__.py +9 -0
  29. specterm1d-0.1.0/specterm1d/commands/colon.py +174 -0
  30. specterm1d-0.1.0/specterm1d/commands/display.py +347 -0
  31. specterm1d-0.1.0/specterm1d/commands/measure.py +139 -0
  32. specterm1d-0.1.0/specterm1d/commands/transform.py +156 -0
  33. specterm1d-0.1.0/specterm1d/cursorscript.py +102 -0
  34. specterm1d-0.1.0/specterm1d/fitting.py +433 -0
  35. specterm1d-0.1.0/specterm1d/io/__init__.py +8 -0
  36. specterm1d-0.1.0/specterm1d/io/pypeit_io.py +179 -0
  37. specterm1d-0.1.0/specterm1d/io/registry.py +81 -0
  38. specterm1d-0.1.0/specterm1d/io/specutils_io.py +100 -0
  39. specterm1d-0.1.0/specterm1d/keymap.py +188 -0
  40. specterm1d-0.1.0/specterm1d/logfile.py +79 -0
  41. specterm1d-0.1.0/specterm1d/plot.py +475 -0
  42. specterm1d-0.1.0/specterm1d/session.py +702 -0
  43. specterm1d-0.1.0/specterm1d/spec.py +214 -0
  44. specterm1d-0.1.0/specterm1d/term/__init__.py +36 -0
  45. specterm1d-0.1.0/specterm1d/term/base.py +92 -0
  46. specterm1d-0.1.0/specterm1d/term/caps.py +229 -0
  47. specterm1d-0.1.0/specterm1d/term/chrome.py +213 -0
  48. specterm1d-0.1.0/specterm1d/term/gui.py +279 -0
  49. specterm1d-0.1.0/specterm1d/term/halfblock.py +128 -0
  50. specterm1d-0.1.0/specterm1d/term/input.py +220 -0
  51. specterm1d-0.1.0/specterm1d/term/iterm2.py +45 -0
  52. specterm1d-0.1.0/specterm1d/term/kitty.py +83 -0
  53. specterm1d-0.1.0/specterm1d/term/sixel.py +151 -0
  54. specterm1d-0.1.0/specterm1d/transcript.py +35 -0
  55. specterm1d-0.1.0/specterm1d/view.py +239 -0
  56. specterm1d-0.1.0/specterm1d.egg-info/PKG-INFO +276 -0
  57. specterm1d-0.1.0/specterm1d.egg-info/SOURCES.txt +91 -0
  58. specterm1d-0.1.0/specterm1d.egg-info/dependency_links.txt +1 -0
  59. specterm1d-0.1.0/specterm1d.egg-info/entry_points.txt +3 -0
  60. specterm1d-0.1.0/specterm1d.egg-info/requires.txt +21 -0
  61. specterm1d-0.1.0/specterm1d.egg-info/scm_file_list.json +87 -0
  62. specterm1d-0.1.0/specterm1d.egg-info/scm_version.json +8 -0
  63. specterm1d-0.1.0/specterm1d.egg-info/top_level.txt +1 -0
  64. specterm1d-0.1.0/tests/__init__.py +0 -0
  65. specterm1d-0.1.0/tests/conftest.py +89 -0
  66. specterm1d-0.1.0/tests/golden/tabular_default.png +0 -0
  67. specterm1d-0.1.0/tests/test_caps.py +375 -0
  68. specterm1d-0.1.0/tests/test_colon.py +130 -0
  69. specterm1d-0.1.0/tests/test_cursorscript.py +70 -0
  70. specterm1d-0.1.0/tests/test_display_commands.py +398 -0
  71. specterm1d-0.1.0/tests/test_docs.py +108 -0
  72. specterm1d-0.1.0/tests/test_end_to_end.py +110 -0
  73. specterm1d-0.1.0/tests/test_fitting.py +152 -0
  74. specterm1d-0.1.0/tests/test_graphics_backends.py +189 -0
  75. specterm1d-0.1.0/tests/test_gui.py +434 -0
  76. specterm1d-0.1.0/tests/test_gui_session.py +395 -0
  77. specterm1d-0.1.0/tests/test_halfblock.py +152 -0
  78. specterm1d-0.1.0/tests/test_input.py +94 -0
  79. specterm1d-0.1.0/tests/test_keymap.py +144 -0
  80. specterm1d-0.1.0/tests/test_logfile.py +89 -0
  81. specterm1d-0.1.0/tests/test_measure.py +143 -0
  82. specterm1d-0.1.0/tests/test_mouse.py +272 -0
  83. specterm1d-0.1.0/tests/test_plot.py +383 -0
  84. specterm1d-0.1.0/tests/test_profiles.py +246 -0
  85. specterm1d-0.1.0/tests/test_pypeit_io.py +129 -0
  86. specterm1d-0.1.0/tests/test_registry.py +100 -0
  87. specterm1d-0.1.0/tests/test_session.py +314 -0
  88. specterm1d-0.1.0/tests/test_spec.py +123 -0
  89. specterm1d-0.1.0/tests/test_specutils_io.py +48 -0
  90. specterm1d-0.1.0/tests/test_text_chrome.py +222 -0
  91. specterm1d-0.1.0/tests/test_transcript.py +65 -0
  92. specterm1d-0.1.0/tests/test_transform.py +139 -0
  93. specterm1d-0.1.0/tests/test_view.py +226 -0
@@ -0,0 +1,67 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ concurrency:
9
+ group: ${{ github.workflow }}-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ jobs:
13
+ check:
14
+ name: ${{ matrix.name }}
15
+ runs-on: ubuntu-latest
16
+ env:
17
+ # matplotlib must never reach for a display in CI.
18
+ MPLBACKEND: Agg
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ include:
23
+ - name: codestyle
24
+ python: "3.14"
25
+ extras: dev
26
+ command: ruff check .
27
+ - name: build_docs
28
+ python: "3.14"
29
+ extras: docs
30
+ command: sphinx-build -W --keep-going docs docs/_build/html
31
+ - name: test (3.14)
32
+ python: "3.14"
33
+ extras: dev
34
+ command: python -m pytest -q
35
+ - name: test (3.13)
36
+ python: "3.13"
37
+ extras: dev
38
+ command: python -m pytest -q
39
+ steps:
40
+ - uses: actions/checkout@v7
41
+ with:
42
+ # setuptools_scm derives the version from the tag history, and a
43
+ # shallow clone has none.
44
+ fetch-depth: 0
45
+ - uses: actions/setup-python@v7
46
+ with:
47
+ python-version: ${{ matrix.python }}
48
+ cache: pip
49
+ - run: pip install -e '.[${{ matrix.extras }}]'
50
+ - run: ${{ matrix.command }}
51
+
52
+ package:
53
+ name: package
54
+ runs-on: ubuntu-latest
55
+ steps:
56
+ - uses: actions/checkout@v7
57
+ with:
58
+ fetch-depth: 0
59
+ - uses: actions/setup-python@v7
60
+ with:
61
+ python-version: "3.14"
62
+ cache: pip
63
+ - run: pip install build twine
64
+ # Catches a broken long description or bad metadata on the pull request
65
+ # that introduces it. On release day the version number is already spent.
66
+ - run: python -m build
67
+ - run: twine check --strict dist/*
@@ -0,0 +1,63 @@
1
+ name: Release
2
+
3
+ # Publishing to PyPI is irreversible: a version number, once used, can never
4
+ # be reused even after the file is deleted. So the artefact is built and
5
+ # checked once, then uploaded from that same artefact - and TestPyPI can be
6
+ # rehearsed by hand before anything reaches the real index.
7
+ on:
8
+ release:
9
+ types: [published]
10
+ workflow_dispatch:
11
+ inputs:
12
+ target:
13
+ description: Index to publish to
14
+ type: choice
15
+ options: [testpypi, pypi]
16
+ default: testpypi
17
+
18
+ jobs:
19
+ build:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v7
23
+ with:
24
+ # setuptools_scm needs the tag to produce a release version rather
25
+ # than a development guess.
26
+ fetch-depth: 0
27
+ - uses: actions/setup-python@v7
28
+ with:
29
+ python-version: "3.14"
30
+ - run: pip install build twine
31
+ - run: python -m build
32
+ - run: twine check --strict dist/*
33
+ - name: Refuse to publish a development version
34
+ # A tagless build produces something like 0.1.dev48+gae58a27, whose
35
+ # local-version segment PyPI rejects outright. Failing here says why,
36
+ # rather than leaving it to a confusing upload error.
37
+ run: |
38
+ version=$(ls dist/*.tar.gz | sed -E 's/.*specterm1d-(.*)\.tar\.gz/\1/')
39
+ echo "built version: $version"
40
+ case "$version" in
41
+ *dev*|*+*) echo "::error::$version is not a release version; tag the commit first"; exit 1 ;;
42
+ esac
43
+ - uses: actions/upload-artifact@v7
44
+ with:
45
+ name: dist
46
+ path: dist/
47
+
48
+ publish:
49
+ needs: build
50
+ runs-on: ubuntu-latest
51
+ # Trusted publishing: PyPI verifies this workflow's OIDC identity, so
52
+ # there is no API token to store or leak.
53
+ environment: ${{ github.event.inputs.target || 'pypi' }}
54
+ permissions:
55
+ id-token: write
56
+ steps:
57
+ - uses: actions/download-artifact@v8
58
+ with:
59
+ name: dist
60
+ path: dist/
61
+ - uses: pypa/gh-action-pypi-publish@v1.14.2
62
+ with:
63
+ repository-url: ${{ github.event.inputs.target == 'testpypi' && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }}
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ *.egg-info/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ .DS_Store
10
+ splot.log
11
+ specterm1d.egg-info/
12
+ .codegraph/
13
+ docs/_build/
@@ -0,0 +1,23 @@
1
+ version: 2
2
+
3
+ build:
4
+ os: ubuntu-24.04
5
+ tools:
6
+ python: "3.14"
7
+ jobs:
8
+ post_checkout:
9
+ # setuptools_scm reads the version from the git tag, and Read the Docs
10
+ # clones shallow with no tags. Without this the version reads as a bare
11
+ # development guess.
12
+ - git fetch --unshallow --tags || git fetch --tags
13
+
14
+ sphinx:
15
+ configuration: docs/conf.py
16
+ fail_on_warning: true
17
+
18
+ python:
19
+ install:
20
+ - method: pip
21
+ path: .
22
+ extra_requirements:
23
+ - docs
@@ -0,0 +1,43 @@
1
+ # Repository Guidelines
2
+
3
+ ## Project Structure & Module Organization
4
+
5
+ The `specterm1d/` package contains the application code. Core spectrum and session state live in
6
+ `spec.py` and `session.py`; terminal interaction is split across `cli.py`, `view.py`, `plot.py`,
7
+ `keymap.py`, and `cursorscript.py`. Keep new behavior close to the module that owns it rather than
8
+ expanding the CLI entry point. Tests live in `tests/` and generally mirror user-facing features,
9
+ while shared fixtures are defined in `tests/conftest.py`. Keybinding documentation belongs in
10
+ `docs/keys.md`. Packaging and tool configuration are centralized in `pyproject.toml`.
11
+
12
+ ## Build, Test, and Development Commands
13
+
14
+ - `python -m pip install -e '.[dev]'` installs the package in editable mode with pytest, coverage,
15
+ and Ruff.
16
+ - `pytest` runs the complete default test suite under `tests/`.
17
+ - `pytest tests/test_fitting.py -q` runs one focused test module during development.
18
+ - `pytest --cov=specterm1d` reports package coverage; the project does not enforce a numeric floor.
19
+ - `ruff check .` checks formatting-adjacent rules, imports, common bugs, and Python 3.13 compatibility.
20
+ - `specterm1d --dump frame.png spectrum.fits` performs a non-interactive rendering smoke test.
21
+
22
+ ## Coding Style & Naming Conventions
23
+
24
+ Use four-space indentation, a 100-character line limit, and Python 3.13 syntax. Ruff enables the
25
+ `E`, `F`, `W`, `I`, `B`, `C4`, `SIM`, and `RUF` rule families; fix warnings instead of adding broad
26
+ ignores. Use `snake_case` for modules, functions, variables, and fixtures; `PascalCase` for classes;
27
+ and `UPPER_CASE` for constants. Keep imports sorted and add concise docstrings where behavior or
28
+ terminal cleanup constraints are not obvious.
29
+
30
+ ## Testing Guidelines
31
+
32
+ Write pytest tests as `tests/test_<feature>.py` with functions named `test_<behavior>`. Prefer
33
+ temporary paths and existing fixtures so tests never write logs or generated spectra into the
34
+ repository. Mark tests needing local PypeIt development-suite data with `@pytest.mark.devsuite`,
35
+ and skip optional dependencies with `pytest.importorskip`. Add regression coverage for every bug
36
+ fix, especially around renderer fallback, input handling, and numerical measurements.
37
+
38
+ ## Commit & Pull Request Guidelines
39
+
40
+ Recent history uses Conventional Commit-style subjects such as `feat:`, `fix:`, and `docs:`. Keep
41
+ subjects imperative and focused on one logical change. Pull requests should explain the user-visible
42
+ effect, list verification commands, and link relevant issues. Include screenshots or dumped frames
43
+ for rendering changes, and call out optional-backend or terminal-specific behavior explicitly.
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, T. E. Pickering
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,276 @@
1
+ Metadata-Version: 2.4
2
+ Name: specterm1d
3
+ Version: 0.1.0
4
+ Summary: Terminal-based 1D spectrum viewer with IRAF splot keybindings
5
+ Author-email: "T. E. Pickering" <te.pickering@gmail.com>
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/tepickering/specterm1d
8
+ Project-URL: Documentation, https://specterm1d.readthedocs.io
9
+ Project-URL: Repository, https://github.com/tepickering/specterm1d
10
+ Project-URL: Issues, https://github.com/tepickering/specterm1d/issues
11
+ Keywords: spectroscopy,astronomy,terminal,splot,fits
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
20
+ Classifier: Topic :: Scientific/Engineering :: Visualization
21
+ Requires-Python: >=3.13
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy>=2.5
25
+ Requires-Dist: scipy>=1.10
26
+ Requires-Dist: matplotlib>=3.7
27
+ Requires-Dist: astropy>=6.0
28
+ Requires-Dist: specutils>=2.0
29
+ Provides-Extra: pypeit
30
+ Requires-Dist: pypeit>=1.14; extra == "pypeit"
31
+ Provides-Extra: sixel
32
+ Requires-Dist: libsixel-python>=0.5; extra == "sixel"
33
+ Provides-Extra: docs
34
+ Requires-Dist: sphinx>=9.1; extra == "docs"
35
+ Requires-Dist: myst-parser>=5.1; extra == "docs"
36
+ Requires-Dist: furo>=2025.12.19; extra == "docs"
37
+ Provides-Extra: dev
38
+ Requires-Dist: pytest>=7.4; extra == "dev"
39
+ Requires-Dist: pytest-cov; extra == "dev"
40
+ Requires-Dist: ruff; extra == "dev"
41
+ Dynamic: license-file
42
+
43
+ # specterm1d
44
+
45
+ [![CI](https://github.com/tepickering/specterm1d/actions/workflows/ci.yml/badge.svg)](https://github.com/tepickering/specterm1d/actions/workflows/ci.yml)
46
+
47
+ A terminal-based viewer for 1D spectra, with IRAF `splot`'s keybindings.
48
+
49
+ Opens anything `specutils` can read — IRAF multispec, `tabular-fits`,
50
+ `wcs1d-fits`, SDSS, HST/COS, HST/STIS, JWST, APOGEE and more — plus pypeit's
51
+ `OneSpec` and `spec1d` products, including echelle files with their orders
52
+ grouped by object.
53
+
54
+ The point is to keep the `splot` muscle memory intact while drawing a real
55
+ matplotlib figure in the terminal, rather than an ASCII approximation of one.
56
+
57
+ ## Install
58
+
59
+ ```bash
60
+ pip install specterm1d # general FITS spectra
61
+ pip install 'specterm1d[pypeit]' # adds OneSpec and spec1d support
62
+ pip install 'specterm1d[sixel]' # optional libsixel encoder
63
+ ```
64
+
65
+ pypeit is deliberately optional. Nothing imports it at module scope, so the
66
+ tool works as a general FITS viewer without it, and files it cannot open are
67
+ reported clearly rather than crashing on an import.
68
+
69
+ Requires Python 3.13+ and numpy 2.5+.
70
+
71
+ ## Quick start
72
+
73
+ ```bash
74
+ specterm1d spec1d_J0935+0924.fits # or the short alias: st1d
75
+ ```
76
+
77
+ Arrow keys move a crosshair, `?` pages the full keymap, `q` quits.
78
+
79
+ | Flag | Effect |
80
+ |------|--------|
81
+ | `--renderer kitty\|iterm2\|sixel\|gui\|halfblock` | force a backend instead of probing |
82
+ | `--units nm` | start in other dispersion units (`um`, `GHz`, anything astropy knows) |
83
+ | `--mouse` / `--no-mouse` | override click/drag positioning (on for inline graphics) |
84
+ | `--format NAME` | force a loader instead of sniffing the file |
85
+ | `--log FILE` | measurement log path (default `splot.log`) |
86
+ | `--cursor FILE` | replay a keystroke script instead of reading the keyboard |
87
+ | `--dump OUT.png` | render one frame to a PNG and exit; needs no terminal |
88
+ | `--dump-size WxH` | pixel size for `--dump` (default `1200x700`) |
89
+ | `--debug` | show full tracebacks instead of one-line errors |
90
+
91
+ ## Terminal support
92
+
93
+ One matplotlib figure is rendered to an RGBA buffer, and five interchangeable
94
+ backends put those pixels on screen. Axes, tick labels, error bands and fit
95
+ overlays therefore look the same everywhere; only the fidelity changes.
96
+
97
+ | Terminal | Backend | Notes |
98
+ |----------|---------|-------|
99
+ | kitty, Ghostty, WezTerm | kitty graphics | pixel-exact; PNG transport |
100
+ | iTerm2 | **graphics window** | its inline image path leaks; see below |
101
+ | Windows Terminal 1.22+, foot, xterm, Konsole, mlterm, contour | sixel | detected via Primary Device Attributes |
102
+ | **stock macOS Terminal, GNOME Terminal, Alacritty** | **graphics window** | no graphics protocol exists; see below |
103
+ | ssh with no display, tmux over ssh | halfblock | always available |
104
+
105
+ The halfblock backend is first-class, not a stub - it is what runs wherever
106
+ neither an inline protocol nor a window is available. Each cell is `▀` with the top source pixel as foreground and the
107
+ bottom as background, giving `cols x 2*rows` effective pixels; frames are
108
+ diffed so a redraw costs only the cells that changed. Terminal.app never
109
+ gained 24-bit colour, so there is an xterm-256 path as well as truecolor.
110
+
111
+ Because that gives a 116x43 window a 116x82 pixel figure, halfblock does not
112
+ let matplotlib draw the axis decoration: a 4pt tick label is 5.6 px tall
113
+ there, which is a smear across three cells at any font size. Instead the
114
+ figure is rendered full bleed with nothing but data, and the terminal paints
115
+ the spines, tick marks, labels, title and legend as its own glyphs at your
116
+ font size. The curve ends up with more pixels than it had when matplotlib was
117
+ spending margins on labels nobody could read.
118
+
119
+ Under tmux the kitty protocol is never probed — its passthrough is unreliable
120
+ — so tmux users get sixel where tmux was built with `--enable-sixel`, and
121
+ halfblock otherwise.
122
+
123
+ ### Two-window mode
124
+
125
+ Terminals with no inline-graphics protocol get a real matplotlib window
126
+ instead of half-block cells. This is what IRAF `splot` did on a
127
+ Tektronix-emulating terminal like `xgterm`: **you point at a feature in the
128
+ graphics window and press a key**, while prompts and measurement results
129
+ scroll past in the text terminal.
130
+
131
+ The terminal is a plain scrolling transcript in this mode — no full-screen
132
+ layout, no raw mode, no pinned status line. The live `x`/`y`/`pix` readout
133
+ moves to the window title, where your eye already is. `?` and `:show` scroll
134
+ past rather than paging.
135
+
136
+ Every binding means the same thing in both modes; that is the point.
137
+
138
+ | terminal | renderer |
139
+ |---|---|
140
+ | kitty, Ghostty, WezTerm | kitty protocol, inline |
141
+ | iTerm2 | graphics window (`--renderer iterm2` to force inline) |
142
+ | xterm with sixel | sixel, inline |
143
+ | Terminal.app, GNOME Terminal, Alacritty | graphics window |
144
+ | xterm on Linux with X11 | graphics window |
145
+ | ssh with no display, tmux over ssh | half-block |
146
+
147
+ Inline graphics still win where the terminal supports them — one window beats
148
+ two — with iTerm2 the one exception. Half-block is the last resort: correct
149
+ everywhere, comfortable nowhere.
150
+
151
+ ### Why iTerm2 gets a window
152
+
153
+ iTerm2 never frees an inline image. Every distinct frame costs it about a
154
+ decoded bitmap of resident memory for the life of the session, so panning a
155
+ spectrum grows the terminal process by roughly 1.7 MB per keystroke — measured
156
+ over 100 cursor moves on iTerm2 3.6.11, against 0.05 MB/frame for the same
157
+ loop drawing text. kitty's protocol replaces a placement in situ through a
158
+ stable image id and does not do this; OSC 1337 has neither an id nor a delete
159
+ verb, and nothing the application can send collects the images. Its sixel path
160
+ leaks too, at 4 MB/frame, so both inline backends step aside where a window is
161
+ available. `--renderer iterm2` still forces the inline path.
162
+
163
+ This is not specific to specterm1d. It has been reported upstream twice —
164
+ [#3943](https://gitlab.com/gnachman/iterm2/-/issues/3943) in 2015 and
165
+ [#10420](https://gitlab.com/gnachman/iterm2/-/issues/10420) in 2022, the
166
+ latter reaching about 20 GB and surviving a scrollback clear and a session
167
+ close — and closed both times. The behaviour is still present in 3.6.11, and
168
+ has driven a machine into the OOM killer at 138 GB. There is no open upstream
169
+ issue to wait on, so the window is where iTerm2 stays.
170
+
171
+ To force either mode:
172
+
173
+ ```
174
+ specterm1d --gui spec1d.fits # or --renderer gui
175
+ specterm1d --renderer halfblock spec1d.fits
176
+ ```
177
+
178
+ The window opens at 1200x800 and is then yours to resize; resizing re-renders
179
+ at the new size. If no window can be opened — no `DISPLAY`, no usable
180
+ toolkit — specterm1d prints one line to stderr and falls back to half-block
181
+ rather than refusing to start.
182
+
183
+ ## Keys
184
+
185
+ The complete reference is in [the key and command reference](https://specterm1d.readthedocs.io/en/latest/keys.html). The most-used:
186
+
187
+ | Key | Action |
188
+ |-----|--------|
189
+ | `<space>` | report the cursor position and nearest pixel |
190
+ | `a` | expand between two marks; the same point twice autoscales everything |
191
+ | `c` / `r` | clear all windowing / redraw keeping it |
192
+ | `z` `,` `.` | zoom by two about the cursor; pan left; pan right |
193
+ | `(` `)` `#` | previous / next spectrum; go to one by index or name |
194
+ | `e` | equivalent width by summation |
195
+ | `m` | mean, RMS and S/N over a region |
196
+ | `k` + `g`/`l`/`v` | fit a gaussian, lorentzian or voigt profile |
197
+ | `h` + `a`/`b`/`c`/`l`/`r`/`k` | equivalent width from a measured width |
198
+ | `s` | boxcar smooth |
199
+ | `U` | undo the last transform |
200
+ | `w` | the gtools window submode |
201
+ | `:` | colon commands (`:units nm`, `:sigma`, `:sky`, `:mask`, …) |
202
+ | `q` | next input spectrum, then exit |
203
+
204
+ Multi-point commands are explicit: press the command key to arm it, then mark
205
+ each point with `<space>`. The crosshair's **y** matters — `e`, `k` and `h`
206
+ take their continuum from the cursor's y at each marked point, which is what
207
+ IRAF's `sumflux.x` does with `eqy1`/`eqy2`.
208
+
209
+ ## Not implemented yet
210
+
211
+ These keys are **registered** and report "not implemented in v1" when pressed.
212
+ They are never silently absent and never rebound to something else, so muscle
213
+ memory cannot misfire:
214
+
215
+ `d` deblend · `t` ICFIT · `f` arithmetic · `i` write to file ·
216
+ `j` set pixel to cursor · `x` etch-a-sketch · `p` linear wavelength scale ·
217
+ `u` user coordinate scale · `y` standard-star overplot
218
+
219
+ ## Differences from splot
220
+
221
+ Three, stated plainly:
222
+
223
+ - **The cursor is always keyboard-driven and optionally mouse-driven.** Arrow
224
+ keys move a 2D crosshair and shift moves further. Inline Kitty, sixel and
225
+ iTerm2 graphics enable click/drag positioning and draw the full crosshair by
226
+ default. Terminals that answer DECRQM for DECSET 1016 - kitty, ghostty and
227
+ the sixel terminals among them - report the pointer in pixels rather than
228
+ cells, so the cursor tracks it instead of snapping to the character grid.
229
+ Terminals that do not, and tmux, keep cell coordinates. Halfblock leaves
230
+ mouse reporting off, having no pixels to place. Use
231
+ `--no-mouse` or `:mouse no` when you want the terminal's normal text selection
232
+ instead.
233
+ - **`%` cycles the extraction/calibration variant** (`OPT/COUNTS`,
234
+ `BOX/COUNTS`, `OPT/FLAM`, …) rather than an image band, which is the useful
235
+ analogue for pypeit products.
236
+ - **`U` undoes a transform.** `splot` has no equivalent: there, `s` is
237
+ destructive with no recovery short of reloading the file.
238
+
239
+ Beyond that, four display features `splot` had no data for: a one-sigma error
240
+ band (`:sigma`), masked-pixel highlighting (`:mask`), sky/telluric/model
241
+ overlays (`:sky`, `:telluric`, `:model`), and inverse-variance weighting of
242
+ profile fits.
243
+
244
+ ## Measurement log
245
+
246
+ Measurements append to `splot.log` in IRAF's own column formats, taken from
247
+ `anshdr.x`, `eqwidth.x`, `gfit.x` and `avgsnr.x` — including the detail that
248
+ the `m` key suppresses the column header. Existing log-parsing scripts keep
249
+ working:
250
+
251
+ ```
252
+ center cont flux eqw core gfwhm lfwhm
253
+ 5183.6 1.234 -0.456 0.37
254
+ avg: 1.5 rms: 0.25 snr: 6.00
255
+ ```
256
+
257
+ `:nolog` stops writing, `:log` resumes, and `:# some text` adds a comment.
258
+
259
+ ## Batch use
260
+
261
+ `--cursor` replays a keystroke script, reproducing `splot`'s `cursor`
262
+ parameter. Combined with `--dump` it runs with no terminal at all:
263
+
264
+ ```bash
265
+ cat > measure.txt <<'EOF'
266
+ 5200 1.0 e
267
+ 5200 1.0 <space>
268
+ 5400 1.0 <space>
269
+ EOF
270
+
271
+ specterm1d spec.fits --cursor measure.txt --log out.log --dump frame.png
272
+ ```
273
+
274
+ ## Licence
275
+
276
+ BSD-3-Clause.