termarium 0.1.6 → 0.1.8
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.
- package/NOTICE +25 -2
- package/README.md +17 -11
- package/npm/scripts/install.js +147 -6
- package/package.json +1 -1
package/NOTICE
CHANGED
|
@@ -6,8 +6,9 @@ https://codeberg.org/astronexus/hyg
|
|
|
6
6
|
https://www.astronexus.com/projects/hyg
|
|
7
7
|
|
|
8
8
|
The HYG Database is licensed under Creative Commons Attribution-ShareAlike 4.0
|
|
9
|
-
International (CC BY-SA 4.0). The filtered file in this repository keeps
|
|
10
|
-
|
|
9
|
+
International (CC BY-SA 4.0). The filtered file in this repository keeps stars
|
|
10
|
+
needed by Termarium's default bright-sky catalog plus the HIP entries required
|
|
11
|
+
by the bundled Chinese sky-culture line catalog. It stores only these fields:
|
|
11
12
|
|
|
12
13
|
hip, proper, ra, dec, mag, ci, con
|
|
13
14
|
|
|
@@ -19,6 +20,28 @@ authoritative nomenclature catalog; it is a convenience index for the TUI.
|
|
|
19
20
|
Termarium code is licensed under MIT. The bundled star data remains licensed
|
|
20
21
|
under CC BY-SA 4.0.
|
|
21
22
|
|
|
23
|
+
Termarium also includes compact Chinese sky-culture figure and line data
|
|
24
|
+
converted from the Stellarium Chinese sky culture.
|
|
25
|
+
|
|
26
|
+
Source:
|
|
27
|
+
https://github.com/Stellarium/stellarium-skycultures/tree/master/chinese
|
|
28
|
+
|
|
29
|
+
The Stellarium Chinese sky culture data is licensed under Creative Commons
|
|
30
|
+
Attribution-ShareAlike. Termarium converts its HIP-number figure paths and
|
|
31
|
+
metadata into compact offline CSV files for the optional Chinese sky-culture
|
|
32
|
+
mode.
|
|
33
|
+
|
|
34
|
+
Termarium also includes compact Chinese star-name and figure-name search
|
|
35
|
+
metadata converted from Celestial Data by Diego Hernangomez.
|
|
36
|
+
|
|
37
|
+
Source:
|
|
38
|
+
https://github.com/dieghernan/celestial_data
|
|
39
|
+
|
|
40
|
+
Celestial Data is licensed under the BSD 3-Clause License. Its traditional
|
|
41
|
+
Chinese constellations and star names are derived from Stellarium skycultures;
|
|
42
|
+
Termarium uses them only as compact search names, pinyin, English labels, and
|
|
43
|
+
HIP mappings.
|
|
44
|
+
|
|
22
45
|
Termarium also includes a converted constellation-line catalog derived from
|
|
23
46
|
ConstellationLines by Marc van der Sluys / hemel.waarnemen.com.
|
|
24
47
|
|
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ Linux x64, and Windows x64.
|
|
|
26
26
|
- Full-screen terminal UI built with Rust, Ratatui, and Crossterm
|
|
27
27
|
- Real HYG v4.2 bright-star subset, bundled for offline use
|
|
28
28
|
- 88 western constellation figures from a bundled CC BY 4.0 line catalog
|
|
29
|
+
- Optional Chinese sky culture with 318 星官 / 二十八宿 figures and traditional Chinese star-name search
|
|
29
30
|
- Offline Messier deep-sky catalog derived from OpenNGC v20260501
|
|
30
31
|
- Time travel, city tour, alias-aware target search, constellation highlight, and tonight panel
|
|
31
32
|
- Crosshair pointer mode for selecting visible stars directly from the sky map
|
|
@@ -36,7 +37,7 @@ Linux x64, and Windows x64.
|
|
|
36
37
|
- Sun altitude, daylight/twilight state, sunset, and next sunrise
|
|
37
38
|
- First-run city search on the globe, plus setup for custom coordinates and timezone
|
|
38
39
|
- RA/Dec to Alt/Az sky projection for the current observer and time
|
|
39
|
-
-
|
|
40
|
+
- Programmer-friendly selectable themes plus ASCII/Unicode character modes
|
|
40
41
|
- English by default, with in-app Chinese toggle
|
|
41
42
|
|
|
42
43
|
## Usage
|
|
@@ -50,6 +51,7 @@ termarium --lat 31.2304 --lon 121.4737 --name Shanghai
|
|
|
50
51
|
termarium --lat 31.2304 --lon 121.4737 --name Shanghai --lang zh
|
|
51
52
|
termarium --lat 31.2304 --lon 121.4737 --name Shanghai --tz Asia/Shanghai
|
|
52
53
|
termarium --theme aurora --charset unicode
|
|
54
|
+
termarium --theme tokyo-night
|
|
53
55
|
termarium --time 2026-05-07T14:00:00Z
|
|
54
56
|
```
|
|
55
57
|
|
|
@@ -88,7 +90,7 @@ v city tour
|
|
|
88
90
|
a toggle animations
|
|
89
91
|
p toggle planets
|
|
90
92
|
d toggle deep-sky objects
|
|
91
|
-
T
|
|
93
|
+
T open theme menu
|
|
92
94
|
u cycle charset
|
|
93
95
|
t toggle English / Chinese
|
|
94
96
|
m toggle moon panel
|
|
@@ -97,10 +99,12 @@ c toggle constellation lines
|
|
|
97
99
|
+ / - adjust limiting magnitude
|
|
98
100
|
```
|
|
99
101
|
|
|
100
|
-
Landscape mode
|
|
102
|
+
Landscape mode, sky orientation, and western / Chinese sky culture are
|
|
103
|
+
available from the settings panel.
|
|
101
104
|
|
|
102
105
|
Search accepts proper names, curated aliases, HIP/HD-style identifiers, western
|
|
103
|
-
constellation names, Chinese
|
|
106
|
+
constellation names, Chinese sky-culture asterisms, traditional Chinese star
|
|
107
|
+
names, planets, and Messier objects.
|
|
104
108
|
For example, `Tau Ceti`, `τ Ceti`, `天仓五`, `HD 10700`, and `HIP 8102` all
|
|
105
109
|
select the same star.
|
|
106
110
|
|
|
@@ -113,13 +117,15 @@ matched city fills the location fields, then the usual save row applies it.
|
|
|
113
117
|
The application code is MIT licensed. The bundled star catalog is a filtered
|
|
114
118
|
subset derived from the HYG Database v4.2 and remains under CC BY-SA 4.0. The
|
|
115
119
|
constellation-line catalog is derived from Marc van der Sluys'
|
|
116
|
-
ConstellationLines data and remains under CC BY 4.0. The
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
ConstellationLines data and remains under CC BY 4.0. The Chinese sky-culture
|
|
121
|
+
figures are converted from Stellarium Chinese sky culture data under CC BY-SA,
|
|
122
|
+
with compact search metadata from Celestial Data under BSD-3-Clause. The
|
|
123
|
+
deep-sky catalog is derived from OpenNGC v20260501 and remains under CC BY-SA
|
|
124
|
+
4.0. See `NOTICE` for attribution. The bundled world coastline and land data is
|
|
125
|
+
derived from Natural Earth 1:110m public domain vector data, and the bundled
|
|
126
|
+
globe color texture is downsampled from NASA Blue Marble Next Generation July
|
|
127
|
+
2004 topography and bathymetry imagery. Planet and solar calculations are local
|
|
128
|
+
approximate algorithms based on NASA/JPL and NOAA public reference formulas.
|
|
123
129
|
|
|
124
130
|
## Development
|
|
125
131
|
|
package/npm/scripts/install.js
CHANGED
|
@@ -11,6 +11,8 @@ const packageRoot = path.resolve(__dirname, "..", "..");
|
|
|
11
11
|
const packageJson = require(path.join(packageRoot, "package.json"));
|
|
12
12
|
const repo = "https://github.com/7b7b7b/termarium";
|
|
13
13
|
const version = packageJson.version;
|
|
14
|
+
const downloadRetries = readPositiveIntegerEnv("TERMARIUM_NPM_DOWNLOAD_RETRIES", 3);
|
|
15
|
+
const downloadStallTimeoutMs = readPositiveIntegerEnv("TERMARIUM_NPM_DOWNLOAD_STALL_TIMEOUT_MS", 60_000);
|
|
14
16
|
|
|
15
17
|
const targets = {
|
|
16
18
|
"darwin-arm64": {
|
|
@@ -63,11 +65,11 @@ async function main() {
|
|
|
63
65
|
|
|
64
66
|
try {
|
|
65
67
|
console.log(`termarium: downloading ${asset}`);
|
|
66
|
-
const archive = await download(archiveUrl);
|
|
68
|
+
const archive = await download(archiveUrl, { label: asset, progress: true });
|
|
67
69
|
fs.writeFileSync(archivePath, archive);
|
|
68
70
|
|
|
69
71
|
if (process.env.TERMARIUM_NPM_SKIP_CHECKSUM !== "1") {
|
|
70
|
-
const checksums = (await download(checksumUrl)).toString("utf8");
|
|
72
|
+
const checksums = (await download(checksumUrl, { label: "SHA256SUMS" })).toString("utf8");
|
|
71
73
|
verifyChecksum(asset, archive, checksums);
|
|
72
74
|
}
|
|
73
75
|
|
|
@@ -129,7 +131,25 @@ function run(command, args) {
|
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
function download(url,
|
|
134
|
+
async function download(url, options = {}) {
|
|
135
|
+
let lastError;
|
|
136
|
+
for (let attempt = 1; attempt <= downloadRetries; attempt += 1) {
|
|
137
|
+
try {
|
|
138
|
+
return await downloadOnce(url, options);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
lastError = error;
|
|
141
|
+
if (attempt >= downloadRetries) {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
console.error(
|
|
145
|
+
`termarium: download failed (${error.message}); retrying ${attempt}/${downloadRetries - 1}`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
throw lastError;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function downloadOnce(url, options = {}, redirects = 0) {
|
|
133
153
|
return new Promise((resolve, reject) => {
|
|
134
154
|
const request = https.get(
|
|
135
155
|
url,
|
|
@@ -150,7 +170,9 @@ function download(url, redirects = 0) {
|
|
|
150
170
|
reject(new Error(`too many redirects while downloading ${url}`));
|
|
151
171
|
return;
|
|
152
172
|
}
|
|
153
|
-
resolve(
|
|
173
|
+
resolve(
|
|
174
|
+
downloadOnce(new URL(response.headers.location, url).toString(), options, redirects + 1)
|
|
175
|
+
);
|
|
154
176
|
return;
|
|
155
177
|
}
|
|
156
178
|
|
|
@@ -161,14 +183,133 @@ function download(url, redirects = 0) {
|
|
|
161
183
|
}
|
|
162
184
|
|
|
163
185
|
const chunks = [];
|
|
164
|
-
response.
|
|
165
|
-
response.on("
|
|
186
|
+
const progress = createDownloadProgress(options, response.headers["content-length"]);
|
|
187
|
+
response.on("data", (chunk) => {
|
|
188
|
+
chunks.push(chunk);
|
|
189
|
+
progress.update(chunk.length);
|
|
190
|
+
});
|
|
191
|
+
response.on("end", () => {
|
|
192
|
+
progress.finish();
|
|
193
|
+
resolve(Buffer.concat(chunks));
|
|
194
|
+
});
|
|
166
195
|
}
|
|
167
196
|
);
|
|
197
|
+
request.setTimeout(downloadStallTimeoutMs, () => {
|
|
198
|
+
request.destroy(new Error(`download stalled for more than ${downloadStallTimeoutMs / 1000}s`));
|
|
199
|
+
});
|
|
168
200
|
request.on("error", reject);
|
|
169
201
|
});
|
|
170
202
|
}
|
|
171
203
|
|
|
204
|
+
function createDownloadProgress(options, contentLength) {
|
|
205
|
+
if (!options.progress) {
|
|
206
|
+
return { update() {}, finish() {} };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const label = options.label ?? "download";
|
|
210
|
+
const total = Number(contentLength);
|
|
211
|
+
const hasTotal = Number.isFinite(total) && total > 0;
|
|
212
|
+
const startedAt = Date.now();
|
|
213
|
+
const useSingleLine = process.stderr.isTTY;
|
|
214
|
+
const intervalMs = useSingleLine ? 100 : 5_000;
|
|
215
|
+
let received = 0;
|
|
216
|
+
let lastRenderAt = 0;
|
|
217
|
+
let finished = false;
|
|
218
|
+
|
|
219
|
+
const render = (force = false) => {
|
|
220
|
+
if (finished) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
if (!force && now - lastRenderAt < intervalMs) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
lastRenderAt = now;
|
|
228
|
+
|
|
229
|
+
const elapsedSeconds = Math.max((now - startedAt) / 1000, 0.001);
|
|
230
|
+
const speed = received / elapsedSeconds;
|
|
231
|
+
const receivedText = formatBytes(received);
|
|
232
|
+
let message;
|
|
233
|
+
|
|
234
|
+
if (hasTotal) {
|
|
235
|
+
const percent = Math.min(received / total, 1);
|
|
236
|
+
const eta = speed > 0 ? (total - received) / speed : 0;
|
|
237
|
+
message = [
|
|
238
|
+
`termarium: ${label}`,
|
|
239
|
+
formatPercent(percent),
|
|
240
|
+
progressBar(percent),
|
|
241
|
+
`${receivedText}/${formatBytes(total)}`,
|
|
242
|
+
`${formatBytes(speed)}/s`,
|
|
243
|
+
`ETA ${formatDuration(eta)}`,
|
|
244
|
+
].join(" ");
|
|
245
|
+
} else {
|
|
246
|
+
message = `termarium: ${label} ${receivedText} ${formatBytes(speed)}/s`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (useSingleLine) {
|
|
250
|
+
process.stderr.write(`\r${message}`);
|
|
251
|
+
} else {
|
|
252
|
+
console.error(message);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
update(chunkLength) {
|
|
258
|
+
received += chunkLength;
|
|
259
|
+
render(false);
|
|
260
|
+
},
|
|
261
|
+
finish() {
|
|
262
|
+
render(true);
|
|
263
|
+
finished = true;
|
|
264
|
+
if (useSingleLine) {
|
|
265
|
+
process.stderr.write("\n");
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function progressBar(percent) {
|
|
272
|
+
const width = 20;
|
|
273
|
+
const filled = Math.max(0, Math.min(width, Math.round(percent * width)));
|
|
274
|
+
return `[${"#".repeat(filled)}${"-".repeat(width - filled)}]`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function formatPercent(percent) {
|
|
278
|
+
return `${Math.round(percent * 100).toString().padStart(3, " ")}%`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function formatBytes(bytes) {
|
|
282
|
+
const units = ["B", "KiB", "MiB", "GiB"];
|
|
283
|
+
let value = bytes;
|
|
284
|
+
let unitIndex = 0;
|
|
285
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
286
|
+
value /= 1024;
|
|
287
|
+
unitIndex += 1;
|
|
288
|
+
}
|
|
289
|
+
const precision = value >= 100 || unitIndex === 0 ? 0 : 1;
|
|
290
|
+
return `${value.toFixed(precision)} ${units[unitIndex]}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function formatDuration(seconds) {
|
|
294
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
295
|
+
return "--";
|
|
296
|
+
}
|
|
297
|
+
if (seconds < 60) {
|
|
298
|
+
return `${Math.ceil(seconds)}s`;
|
|
299
|
+
}
|
|
300
|
+
const minutes = Math.floor(seconds / 60);
|
|
301
|
+
const remainingSeconds = Math.ceil(seconds % 60);
|
|
302
|
+
return `${minutes}m${remainingSeconds.toString().padStart(2, "0")}s`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function readPositiveIntegerEnv(name, fallback) {
|
|
306
|
+
const value = Number(process.env[name]);
|
|
307
|
+
if (Number.isInteger(value) && value > 0) {
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
return fallback;
|
|
311
|
+
}
|
|
312
|
+
|
|
172
313
|
function escapePowerShell(value) {
|
|
173
314
|
return value.replace(/'/g, "''");
|
|
174
315
|
}
|
package/package.json
CHANGED