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 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 only
10
- the fields needed by Termarium:
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
- - Four themes plus ASCII/Unicode character modes
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 cycle theme
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 and sky orientation are available from the settings panel.
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 constellation names, planets, and Messier objects.
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 deep-sky catalog is
117
- derived from OpenNGC v20260501 and remains under CC BY-SA 4.0. See `NOTICE` for
118
- attribution. The bundled world coastline and land data is derived from Natural
119
- Earth 1:110m public domain vector data, and the bundled globe color texture is
120
- downsampled from NASA Blue Marble Next Generation July 2004 topography and
121
- bathymetry imagery. Planet and solar calculations are local approximate
122
- algorithms based on NASA/JPL and NOAA public reference formulas.
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
 
@@ -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, redirects = 0) {
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(download(new URL(response.headers.location, url).toString(), redirects + 1));
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.on("data", (chunk) => chunks.push(chunk));
165
- response.on("end", () => resolve(Buffer.concat(chunks)));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termarium",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Termarium 穹顶终端: a quiet terminal planetarium.",
5
5
  "license": "MIT AND CC-BY-SA-4.0 AND CC-BY-4.0",
6
6
  "homepage": "https://github.com/7b7b7b/termarium#readme",