@sayue_ltr/fleq 1.51.0 → 2.0.1

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 (33) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +1 -1
  3. package/dist/config.js +2 -2
  4. package/dist/dmdata/telegram-parser.js +78 -5
  5. package/dist/engine/cli/cli-run.js +19 -4
  6. package/dist/engine/eew/eew-tracker.js +41 -15
  7. package/dist/engine/monitor/monitor.js +1 -1
  8. package/dist/engine/notification/notifier.js +5 -3
  9. package/dist/engine/notification/sound-player.js +232 -35
  10. package/dist/engine/presentation/processors/process-eew.js +15 -0
  11. package/dist/engine/presentation/processors/process-message.js +2 -2
  12. package/dist/engine/startup/update-checker.js +23 -2
  13. package/dist/engine/template/compiler.js +4 -1
  14. package/dist/engine/template/field-accessor.js +10 -4
  15. package/dist/engine/template/filters.js +14 -8
  16. package/dist/engine/template/parser.js +10 -15
  17. package/dist/types.js +1 -1
  18. package/dist/ui/earthquake-formatter.js +5 -0
  19. package/dist/ui/formatter.js +49 -0
  20. package/dist/ui/repl-handlers/command-definitions.js +11 -4
  21. package/dist/ui/repl-handlers/info-handlers.js +97 -41
  22. package/dist/ui/repl-handlers/settings-handlers.js +19 -15
  23. package/dist/ui/statistics-formatter.js +65 -15
  24. package/dist/ui/status-line.js +11 -0
  25. package/dist/ui/test-samples.js +6 -0
  26. package/dist/ui/theme.js +9 -0
  27. package/dist/ui/waiting-tips-eew.js +63 -0
  28. package/dist/ui/waiting-tips-info-systems.js +81 -0
  29. package/dist/ui/waiting-tips-seismology.js +97 -0
  30. package/dist/ui/waiting-tips-tsunami.js +72 -0
  31. package/dist/ui/waiting-tips-weather.js +189 -0
  32. package/dist/ui/waiting-tips.js +137 -6
  33. package/package.json +1 -1
package/dist/types.js CHANGED
@@ -23,7 +23,7 @@ exports.DEFAULT_CONFIG = {
23
23
  volcano: true,
24
24
  },
25
25
  sound: true,
26
- eewLog: true,
26
+ eewLog: false,
27
27
  eewLogFields: {
28
28
  hypocenter: true,
29
29
  originTime: true,
@@ -381,6 +381,11 @@ function displayEarthquakeInfo(info) {
381
381
  buf.push(wl);
382
382
  }
383
383
  }
384
+ // EventID (同一地震の紐付け用、通常モードのみ表示)
385
+ if (info.eventId) {
386
+ buf.push((0, formatter_1.frameDivider)(level, width));
387
+ buf.push((0, formatter_1.frameLine)(level, chalk_1.default.gray(`EventID: ${info.eventId}`), width));
388
+ }
384
389
  // フッター
385
390
  (0, formatter_1.renderFooter)(level, info.type, info.reportDateTime, info.publishingOffice, width, buf);
386
391
  buf.push((0, formatter_1.frameBottom)(level, width));
@@ -67,6 +67,7 @@ exports.collectHighlightSpans = collectHighlightSpans;
67
67
  exports.highlightAndWrap = highlightAndWrap;
68
68
  exports.formatTimestamp = formatTimestamp;
69
69
  exports.formatElapsedTime = formatElapsedTime;
70
+ exports.formatUptime = formatUptime;
70
71
  exports.intensityColor = intensityColor;
71
72
  exports.lgIntensityColor = lgIntensityColor;
72
73
  exports.lgIntToNumeric = lgIntToNumeric;
@@ -619,6 +620,54 @@ function formatElapsedTime(ms) {
619
620
  const ss = totalSec % 60;
620
621
  return `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}:${String(ss).padStart(2, "0")}`;
621
622
  }
623
+ /**
624
+ * 稼働時間を "DDD:HH:MM:SS" 形式に整形する。
625
+ * 未使用の上位ゼロ桁は dim (gray) 表示にして画面ヤケを軽減する。
626
+ * 日数が 3 桁を超える場合は桁数が増える。
627
+ */
628
+ function formatUptime(ms) {
629
+ const safeMs = Math.max(0, ms);
630
+ const totalSec = Math.floor(safeMs / 1000);
631
+ const dd = Math.floor(totalSec / 86400);
632
+ const hh = Math.floor((totalSec % 86400) / 3600);
633
+ const mm = Math.floor((totalSec % 3600) / 60);
634
+ const ss = totalSec % 60;
635
+ const ddStr = String(dd).padStart(3, "0");
636
+ const hhStr = String(hh).padStart(2, "0");
637
+ const mmStr = String(mm).padStart(2, "0");
638
+ const ssStr = String(ss).padStart(2, "0");
639
+ const raw = `${ddStr}:${hhStr}:${mmStr}:${ssStr}`;
640
+ // セグメント境界: DDD(0-2) : HH(4-5) : MM(7-8) : SS(10-11)
641
+ // segStarts は dd < 1000 (3桁) の通常ケース用。
642
+ // dd >= 1000 では firstNonZero=0 → activeSegStart=0 で全体 white になるため問題なし。
643
+ const segStarts = [0, 4, 7, 10];
644
+ // 先頭から連続するゼロと区切り文字をスキャン
645
+ let firstNonZero = -1;
646
+ for (let i = 0; i < raw.length; i++) {
647
+ if (raw[i] !== "0" && raw[i] !== ":") {
648
+ firstNonZero = i;
649
+ break;
650
+ }
651
+ }
652
+ // dim 境界を決定
653
+ // - 日部分 (index 0-2): 文字レベルで先頭ゼロをグレーアウト
654
+ // - 時刻部分 (HH/MM/SS): セグメント境界でグレーアウト
655
+ // - 全桁ゼロ: SS セグメントから通常表示
656
+ let dimEnd;
657
+ if (firstNonZero === -1) {
658
+ dimEnd = segStarts[segStarts.length - 1];
659
+ }
660
+ else if (firstNonZero <= 2) {
661
+ dimEnd = firstNonZero;
662
+ }
663
+ else {
664
+ dimEnd = segStarts.filter((s) => s <= firstNonZero).pop();
665
+ }
666
+ if (dimEnd === 0) {
667
+ return chalk_1.default.white(raw);
668
+ }
669
+ return chalk_1.default.gray(raw.slice(0, dimEnd)) + chalk_1.default.white(raw.slice(dimEnd));
670
+ }
622
671
  /** 区切り線 (後方互換用、新コードではフレーム関数を使用) */
623
672
  function separator(char = "─", len = 60) {
624
673
  return chalk_1.default.gray(char.repeat(len));
@@ -41,11 +41,17 @@ const ops = __importStar(require("./operation-handlers"));
41
41
  function buildCommandMap(getCtx) {
42
42
  return {
43
43
  help: {
44
- description: "コマンド一覧を表示 (例: help status)",
45
- detail: "引数なしで一覧表示。help <command> でコマンドの詳細を表示。",
44
+ description: "コマンドの詳細を表示 (例: help notify)",
45
+ detail: "help <command>: コマンドの詳細を表示\n help <command> <sub>: サブコマンドの詳細を表示\n 一覧は commands コマンドで表示できます。",
46
46
  category: "info",
47
47
  handler: (args) => info.handleHelp(getCtx(), args),
48
48
  },
49
+ commands: {
50
+ description: "コマンド一覧を表示 (例: commands settings)",
51
+ detail: "引数なし: 全コマンドをカテゴリ別に一覧表示\n commands <category>: カテゴリで絞り込み (info / status / settings / operation)\n commands <query>: 名前・説明で検索",
52
+ category: "info",
53
+ handler: (args) => info.handleCommands(getCtx(), args),
54
+ },
49
55
  "?": {
50
56
  description: "help のエイリアス",
51
57
  category: "info",
@@ -185,12 +191,13 @@ function buildCommandMap(getCtx) {
185
191
  handler: (args) => settings.handleFocus(getCtx(), args),
186
192
  },
187
193
  clock: {
188
- description: "プロンプト時計の切替 (例: clock / clock elapsed)",
189
- detail: "clock: 経過時間/現在時刻をトグル切替\n clock elapsed: 経過時間表示 (デフォルト)\n clock now: 現在時刻表示",
194
+ description: "プロンプト時計の切替 (例: clock / clock uptime)",
195
+ detail: "clock: 経過時間→現在時刻→稼働時間をトグル切替\n clock elapsed: 経過時間表示 (デフォルト)\n clock now: 現在時刻表示\n clock uptime: 稼働時間表示 (DDD:HH:MM:SS)",
190
196
  category: "settings",
191
197
  subcommands: {
192
198
  elapsed: { description: "経過時間表示 (デフォルト)" },
193
199
  now: { description: "現在時刻表示" },
200
+ uptime: { description: "稼働時間表示 (DDD:HH:MM:SS)" },
194
201
  },
195
202
  handler: (args) => settings.handleClock(getCtx(), args),
196
203
  },
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.CATEGORY_ALIASES = exports.COMMAND_ALIASES = void 0;
40
40
  exports.getCurrentSettingValues = getCurrentSettingValues;
41
41
  exports.handleDetail = handleDetail;
42
+ exports.handleCommands = handleCommands;
42
43
  exports.handleHelp = handleHelp;
43
44
  exports.handleStats = handleStats;
44
45
  exports.handleHistory = handleHistory;
@@ -161,6 +162,7 @@ function printColorGrid(termWidth, items, renderFn) {
161
162
  }
162
163
  /** コマンドのエイリアス (逆引き用) */
163
164
  exports.COMMAND_ALIASES = {
165
+ cmds: "commands",
164
166
  hist: "history",
165
167
  cols: "colors",
166
168
  det: "detail",
@@ -224,8 +226,11 @@ function getCurrentSettingValues(ctx) {
224
226
  options: "normal / compact",
225
227
  },
226
228
  clock: {
227
- current: statusLine.getClockMode() === "clock" ? "現在時刻" : "経過時間",
228
- options: "elapsed / now",
229
+ current: (() => {
230
+ const m = statusLine.getClockMode();
231
+ return m === "clock" ? "現在時刻" : m === "uptime" ? "稼働時間" : "経過時間";
232
+ })(),
233
+ options: "elapsed / now / uptime",
229
234
  },
230
235
  notify: {
231
236
  current: `${onCount}/${totalCount} ON${muteInfo}`,
@@ -274,6 +279,88 @@ function handleDetail(ctx, args) {
274
279
  }
275
280
  console.log(chalk_1.default.yellow(` 不明なサブコマンド: ${sub}`) + chalk_1.default.gray(" (利用可能: tsunami)"));
276
281
  }
282
+ /** カテゴリ名を解決する (日本語ラベルにも対応) */
283
+ function resolveCategory(input) {
284
+ const lower = input.toLowerCase();
285
+ const categories = Object.keys(types_2.CATEGORY_LABELS);
286
+ const exact = categories.find((c) => c === lower);
287
+ if (exact != null)
288
+ return exact;
289
+ for (const cat of categories) {
290
+ if (types_2.CATEGORY_LABELS[cat] === input)
291
+ return cat;
292
+ }
293
+ return null;
294
+ }
295
+ function handleCommands(ctx, args) {
296
+ const trimmed = args.trim();
297
+ let filterCategory = null;
298
+ let searchQuery = null;
299
+ if (trimmed.length > 0) {
300
+ filterCategory = resolveCategory(trimmed);
301
+ if (filterCategory == null) {
302
+ searchQuery = trimmed.toLowerCase();
303
+ }
304
+ }
305
+ // 検索モード
306
+ if (searchQuery != null) {
307
+ const query = searchQuery;
308
+ const matches = Object.entries(ctx.commands)
309
+ .filter(([name]) => name !== "?" && name !== "exit" && name !== "cmds")
310
+ .filter(([name, entry]) => name.toLowerCase().includes(query) ||
311
+ entry.description.toLowerCase().includes(query))
312
+ .sort(([a], [b]) => a.localeCompare(b));
313
+ if (matches.length === 0) {
314
+ console.log(chalk_1.default.yellow(` "${trimmed}" に一致するコマンドはありません`));
315
+ return;
316
+ }
317
+ console.log();
318
+ console.log(chalk_1.default.cyan.bold(` 検索結果: "${trimmed}"`));
319
+ console.log(chalk_1.default.gray(` help <command> で各コマンドの詳細を表示`));
320
+ console.log();
321
+ for (const [name, entry] of matches) {
322
+ const sub = entry.subcommands != null ? chalk_1.default.cyan("+ ") : " ";
323
+ const alias = Object.entries(exports.COMMAND_ALIASES).find(([, v]) => v === name)?.[0];
324
+ const aliasSuffix = alias != null ? chalk_1.default.gray(` (${alias})`) : "";
325
+ console.log(chalk_1.default.white(` ${sub}${name.padEnd(14)}`) + chalk_1.default.gray(entry.description) + aliasSuffix);
326
+ }
327
+ console.log();
328
+ return;
329
+ }
330
+ // 一覧モード
331
+ const currentValues = getCurrentSettingValues(ctx);
332
+ const displayed = new Set();
333
+ const categoryOrder = ["info", "status", "settings", "operation"];
334
+ const categories = filterCategory != null ? [filterCategory] : categoryOrder;
335
+ console.log();
336
+ console.log(chalk_1.default.cyan.bold(" 利用可能なコマンド:"));
337
+ console.log(chalk_1.default.gray(` help <command> で各コマンドの詳細を表示`));
338
+ for (const category of categories) {
339
+ console.log();
340
+ console.log(chalk_1.default.cyan(` [${types_2.CATEGORY_LABELS[category]}]`));
341
+ const commandNames = Object.keys(ctx.commands)
342
+ .filter((name) => name !== "exit" && name !== "?" && name !== "cmds" && ctx.commands[name].category === category)
343
+ .sort();
344
+ for (const name of commandNames) {
345
+ if (displayed.has(name))
346
+ continue;
347
+ displayed.add(name);
348
+ const entry = ctx.commands[name];
349
+ const sub = entry.subcommands != null ? chalk_1.default.cyan("+ ") : " ";
350
+ const setting = currentValues[name];
351
+ const valueSuffix = setting != null
352
+ ? chalk_1.default.gray(" [") + chalk_1.default.yellow(setting.current) + chalk_1.default.gray("]")
353
+ : "";
354
+ const alias = Object.entries(exports.COMMAND_ALIASES).find(([, v]) => v === name)?.[0];
355
+ const aliasSuffix = alias != null ? chalk_1.default.gray(` (${alias})`) : "";
356
+ console.log(chalk_1.default.white(` ${sub}${name.padEnd(14)}`) + chalk_1.default.gray(entry.description) + aliasSuffix + valueSuffix);
357
+ }
358
+ }
359
+ console.log();
360
+ console.log(chalk_1.default.cyan(" +") + chalk_1.default.gray(" = サブコマンドあり (help <command> で詳細表示)"));
361
+ console.log(chalk_1.default.gray(" カテゴリ絞り込み: commands <category> / 検索: commands <query>"));
362
+ console.log();
363
+ }
277
364
  function handleHelp(ctx, args) {
278
365
  const trimmed = args.trim();
279
366
  if (trimmed.length > 0) {
@@ -284,13 +371,15 @@ function handleHelp(ctx, args) {
284
371
  return;
285
372
  }
286
373
  if (parts.length > 1 && entry.subcommands) {
287
- const sub = entry.subcommands[parts[1]];
374
+ const subInput = parts[1].toLowerCase();
375
+ const subKey = Object.keys(entry.subcommands).find((k) => k.toLowerCase() === subInput);
376
+ const sub = subKey != null ? entry.subcommands[subKey] : undefined;
288
377
  if (sub == null) {
289
378
  console.log(chalk_1.default.yellow(` 不明なサブコマンド: ${parts[0]} ${parts[1]}`));
290
379
  return;
291
380
  }
292
381
  console.log();
293
- console.log(chalk_1.default.cyan.bold(` ${parts[0]} ${parts[1]}`) + chalk_1.default.gray(` — ${sub.description}`));
382
+ console.log(chalk_1.default.cyan.bold(` ${parts[0]} ${subKey}`) + chalk_1.default.gray(` — ${sub.description}`));
294
383
  if (sub.detail) {
295
384
  console.log();
296
385
  for (const line of sub.detail.split("\n")) {
@@ -321,45 +410,12 @@ function handleHelp(ctx, args) {
321
410
  console.log();
322
411
  return;
323
412
  }
324
- // help — カテゴリ別一覧
413
+ // help — 引数なしはガイド表示
325
414
  console.log();
326
- console.log(chalk_1.default.cyan.bold(" 利用可能なコマンド:"));
327
- const currentValues = getCurrentSettingValues(ctx);
328
- const displayed = new Set();
329
- const categoryOrder = ["info", "status", "settings", "operation"];
330
- for (const category of categoryOrder) {
331
- console.log();
332
- console.log(chalk_1.default.cyan(` [${types_2.CATEGORY_LABELS[category]}]`));
333
- const commandNames = Object.keys(ctx.commands)
334
- .filter((name) => name !== "exit" && name !== "?" && ctx.commands[name].category === category)
335
- .sort();
336
- for (const name of commandNames) {
337
- const entry = ctx.commands[name];
338
- if (displayed.has(entry.description))
339
- continue;
340
- displayed.add(entry.description);
341
- const setting = currentValues[name];
342
- const valueSuffix = setting != null
343
- ? chalk_1.default.gray(" [") + chalk_1.default.yellow(setting.current) + chalk_1.default.gray("]") +
344
- (setting.options ? chalk_1.default.gray(` (${setting.options})`) : "")
345
- : "";
346
- const alias = Object.entries(exports.COMMAND_ALIASES)
347
- .find(([, v]) => v === name)?.[0];
348
- const aliasSuffix = alias != null ? chalk_1.default.gray(` (${alias})`) : "";
349
- console.log(chalk_1.default.white(` ${name.padEnd(14)}`) + chalk_1.default.gray(entry.description) + aliasSuffix + valueSuffix);
350
- if (entry.subcommands) {
351
- const subNames = Object.keys(entry.subcommands).sort();
352
- for (let i = 0; i < subNames.length; i++) {
353
- const subName = subNames[i];
354
- const sub = entry.subcommands[subName];
355
- const prefix = i < subNames.length - 1 ? "├─" : "└─";
356
- console.log(chalk_1.default.gray(` ${prefix} `) + chalk_1.default.white(subName.padEnd(10)) + chalk_1.default.gray(sub.description));
357
- }
358
- }
359
- }
360
- }
415
+ console.log(chalk_1.default.cyan.bold(" help <command>") + chalk_1.default.gray(" — コマンドの詳細を表示"));
416
+ console.log(chalk_1.default.cyan.bold(" commands") + chalk_1.default.gray(" — コマンド一覧を表示"));
361
417
  console.log();
362
- console.log(chalk_1.default.gray(" エイリアス: ") + chalk_1.default.white("?") + chalk_1.default.gray(" → help, ") + chalk_1.default.white("exit") + chalk_1.default.gray(" → quit (コマンド名の大文字小文字は区別しません)"));
418
+ console.log(chalk_1.default.gray(" 例: help notify, help test table, commands settings"));
363
419
  console.log();
364
420
  }
365
421
  function handleStats(ctx) {
@@ -408,31 +408,35 @@ function handleFocus(ctx, args) {
408
408
  }
409
409
  function handleClock(ctx, args) {
410
410
  const trimmed = args.trim();
411
+ const labels = {
412
+ clock: "現在時刻",
413
+ elapsed: "経過時間",
414
+ uptime: "稼働時間",
415
+ };
411
416
  if (trimmed.length === 0) {
412
417
  const current = ctx.statusLine.getClockMode();
413
- const next = current === "elapsed" ? "clock" : "elapsed";
418
+ const next = current === "elapsed" ? "clock" : current === "clock" ? "uptime" : "elapsed";
414
419
  ctx.statusLine.setClockMode(next);
415
420
  ctx.config.promptClock = next;
416
421
  ctx.updateConfig((c) => { c.promptClock = next; });
417
- const label = next === "clock" ? "現在時刻" : "経過時間";
418
- console.log(` プロンプト時計を ${label} に切り替えました。`);
422
+ console.log(` プロンプト時計を ${labels[next]} に切り替えました。`);
419
423
  return;
420
424
  }
421
425
  const clockLower = trimmed.toLowerCase();
422
- if (clockLower === "elapsed") {
423
- ctx.statusLine.setClockMode("elapsed");
424
- ctx.config.promptClock = "elapsed";
425
- ctx.updateConfig((c) => { c.promptClock = "elapsed"; });
426
- console.log(" プロンプト時計を 経過時間 に変更しました。");
427
- }
428
- else if (clockLower === "now") {
429
- ctx.statusLine.setClockMode("clock");
430
- ctx.config.promptClock = "clock";
431
- ctx.updateConfig((c) => { c.promptClock = "clock"; });
432
- console.log(" プロンプト時計を 現在時刻 に変更しました。");
426
+ const modeMap = {
427
+ elapsed: "elapsed",
428
+ now: "clock",
429
+ uptime: "uptime",
430
+ };
431
+ const mode = modeMap[clockLower];
432
+ if (mode) {
433
+ ctx.statusLine.setClockMode(mode);
434
+ ctx.config.promptClock = mode;
435
+ ctx.updateConfig((c) => { c.promptClock = mode; });
436
+ console.log(` プロンプト時計を ${labels[mode]} に変更しました。`);
433
437
  }
434
438
  else {
435
- console.log(chalk_1.default.yellow(" elapsed または now を指定してください。"));
439
+ console.log(chalk_1.default.yellow(" elapsed, now, uptime のいずれかを指定してください。"));
436
440
  }
437
441
  }
438
442
  function handleTipInterval(ctx, args) {
@@ -1,8 +1,42 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.formatStatsDuration = formatStatsDuration;
4
37
  exports.displayStatistics = displayStatistics;
5
38
  const formatter_1 = require("./formatter");
39
+ const theme = __importStar(require("./theme"));
6
40
  // ── 定数 ──
7
41
  const TYPE_LABELS = {
8
42
  VXSE43: "緊急地震速報(警報)",
@@ -42,6 +76,14 @@ const CATEGORY_LABELS = {
42
76
  nankaiTrough: "南海トラフ",
43
77
  other: "その他",
44
78
  };
79
+ const CATEGORY_ROLE = {
80
+ eew: "statsCategoryEew",
81
+ earthquake: "statsCategoryEarthquake",
82
+ tsunami: "statsCategoryTsunami",
83
+ volcano: "statsCategoryVolcano",
84
+ nankaiTrough: "statsCategoryNankaiTrough",
85
+ other: "statsCategoryOther",
86
+ };
45
87
  const CATEGORY_ORDER = [
46
88
  "eew",
47
89
  "earthquake",
@@ -52,6 +94,13 @@ const CATEGORY_ORDER = [
52
94
  ];
53
95
  const INTENSITY_ORDER = ["1", "2", "3", "4", "5-", "5+", "6-", "6+", "7"];
54
96
  const FRAME_LEVEL = "info";
97
+ // ── chalk ショートカット ──
98
+ function muted(s) {
99
+ return theme.getRoleChalk("statsMuted")(s);
100
+ }
101
+ function count(s) {
102
+ return theme.getRoleChalk("statsCount")(s);
103
+ }
55
104
  // ── 公開関数 ──
56
105
  /** 経過時間をミリ秒から日本語の文字列に変換する */
57
106
  function formatStatsDuration(ms) {
@@ -78,7 +127,7 @@ function displayStatistics(snapshot, now) {
78
127
  const elapsedMs = effectiveNow.getTime() - snapshot.startTime.getTime();
79
128
  if (snapshot.totalCount === 0) {
80
129
  const title = "統計";
81
- const msg = "まだ電文を受信していません";
130
+ const msg = muted("まだ電文を受信していません");
82
131
  const width = calcWidth([title, msg]);
83
132
  console.log((0, formatter_1.frameTop)(FRAME_LEVEL, width));
84
133
  console.log((0, formatter_1.frameLine)(FRAME_LEVEL, title, width));
@@ -133,7 +182,7 @@ function calcWidth(contentLines) {
133
182
  // frameLine adds 4 chars overhead (│ + space + space + │)
134
183
  return Math.max(40, Math.min(200, maxContentWidth + 4));
135
184
  }
136
- /** 最大震度内訳行を構築する */
185
+ /** 最大震度内訳行を構築する(色付き) */
137
186
  function buildIntBreakdownLine(earthquakeMaxIntByEvent) {
138
187
  const intCounts = new Map();
139
188
  for (const maxInt of earthquakeMaxIntByEvent.values()) {
@@ -141,12 +190,13 @@ function buildIntBreakdownLine(earthquakeMaxIntByEvent) {
141
190
  }
142
191
  const parts = [];
143
192
  for (const intensity of INTENSITY_ORDER) {
144
- const count = intCounts.get(intensity);
145
- if (count != null && count > 0) {
146
- parts.push(`${intensity}:${count}`);
193
+ const cnt = intCounts.get(intensity);
194
+ if (cnt != null && cnt > 0) {
195
+ const intStyle = (0, formatter_1.intensityColor)(intensity);
196
+ parts.push(`${intStyle(intensity)}${muted(":")}${cnt}`);
147
197
  }
148
198
  }
149
- return ` 最大震度内訳 ${parts.join(" ")}`;
199
+ return ` ${muted("最大震度内訳")} ${parts.join(" ")}`;
150
200
  }
151
201
  /** 全コンテンツ行を構築する (__DIVIDER__ はフレーム区切り線のセンチネル) */
152
202
  function buildAllContentLines(snapshot, activeCategories, typesByCategory, elapsedMs, countWidth) {
@@ -159,21 +209,21 @@ function buildAllContentLines(snapshot, activeCategories, typesByCategory, elaps
159
209
  hour: "2-digit",
160
210
  minute: "2-digit",
161
211
  });
162
- lines.push(`開始: ${startStr} 経過: ${formatStatsDuration(elapsedMs)} 合計: ${snapshot.totalCount}件`);
212
+ lines.push(`${muted("開始:")} ${startStr} ${muted("経過:")} ${formatStatsDuration(elapsedMs)} ${muted("合計:")} ${count(String(snapshot.totalCount))}件`);
163
213
  // カテゴリセクション
164
- for (let i = 0; i < activeCategories.length; i++) {
165
- const category = activeCategories[i];
214
+ for (const category of activeCategories) {
166
215
  lines.push("__DIVIDER__");
167
216
  // カテゴリヘッダー
168
217
  const types = typesByCategory.get(category) ?? [];
169
218
  const categoryCount = types.reduce((sum, t) => sum + (snapshot.countByType.get(t) ?? 0), 0);
170
219
  const catLabel = CATEGORY_LABELS[category];
220
+ const catStyle = theme.getRoleChalk(CATEGORY_ROLE[category]);
171
221
  let catHeader;
172
222
  if (category === "eew") {
173
- catHeader = `[${catLabel}] ${categoryCount}件 / ${snapshot.eewEventCount}イベント`;
223
+ catHeader = `${catStyle(`[${catLabel}]`)} ${count(String(categoryCount))}件 / ${count(String(snapshot.eewEventCount))}イベント`;
174
224
  }
175
225
  else {
176
- catHeader = `[${catLabel}] ${categoryCount}件`;
226
+ catHeader = `${catStyle(`[${catLabel}]`)} ${count(String(categoryCount))}件`;
177
227
  }
178
228
  lines.push(catHeader);
179
229
  // タイプ行
@@ -190,14 +240,14 @@ function buildAllContentLines(snapshot, activeCategories, typesByCategory, elaps
190
240
  maxLabelWidth = (0, formatter_1.visualWidth)(label);
191
241
  }
192
242
  for (const headType of types) {
193
- const count = snapshot.countByType.get(headType) ?? 0;
194
- if (count === 0)
243
+ const cnt = snapshot.countByType.get(headType) ?? 0;
244
+ if (cnt === 0)
195
245
  continue;
196
246
  const label = TYPE_LABELS[headType] ?? headType;
197
247
  const typePad = " ".repeat(Math.max(0, maxTypeWidth - (0, formatter_1.visualWidth)(headType)));
198
248
  const labelPad = " ".repeat(Math.max(0, maxLabelWidth - (0, formatter_1.visualWidth)(label)));
199
- const countStr = String(count).padStart(countWidth);
200
- lines.push(` ${headType}${typePad} ${label}${labelPad} : ${countStr}`);
249
+ const countStr = String(cnt).padStart(countWidth);
250
+ lines.push(` ${muted(headType)}${typePad} ${label}${labelPad} ${muted(":")} ${count(countStr)}`);
201
251
  }
202
252
  // 地震カテゴリの場合は最大震度内訳を追加
203
253
  if (category === "earthquake" && snapshot.earthquakeMaxIntByEvent.size > 0) {
@@ -45,6 +45,17 @@ class StatusLine {
45
45
  */
46
46
  buildPrefix(options) {
47
47
  const suffix = options?.noSuffix ? "" : chalk_1.default.gray("]> ");
48
+ // uptime モードは接続状態に依存しない
49
+ if (this.clockMode === "uptime") {
50
+ const dot = this.connectedAt == null
51
+ ? chalk_1.default.gray("○")
52
+ : this.pulseOn ? chalk_1.default.cyan("●") : chalk_1.default.gray("○");
53
+ return (chalk_1.default.gray("FlEq [") +
54
+ dot +
55
+ chalk_1.default.gray(" ") +
56
+ (0, formatter_1.formatUptime)(process.uptime() * 1000) +
57
+ suffix);
58
+ }
48
59
  if (this.connectedAt == null) {
49
60
  return (chalk_1.default.gray("FlEq [") + chalk_1.default.gray("○ --:--:--") + suffix);
50
61
  }
@@ -116,6 +116,7 @@ exports.SAMPLE_EARTHQUAKE = {
116
116
  reportDateTime: "2024/01/01 00:00:00",
117
117
  headline: "1日00時00分ころ、地震がありました。",
118
118
  publishingOffice: "気象庁",
119
+ eventId: "20240101000000",
119
120
  earthquake: {
120
121
  originTime: "2024/01/01 00:00:00",
121
122
  hypocenterName: "石川県能登地方",
@@ -280,6 +281,7 @@ const FALLBACK_EARTHQUAKE_WARNING = {
280
281
  reportDateTime: "2024/01/02 10:00:00",
281
282
  headline: "長野県北部で震度4を観測しました。",
282
283
  publishingOffice: "気象庁",
284
+ eventId: null,
283
285
  earthquake: {
284
286
  originTime: "2024/01/02 09:58:00",
285
287
  hypocenterName: "長野県北部",
@@ -305,6 +307,7 @@ const FALLBACK_EARTHQUAKE_CANCEL = {
305
307
  reportDateTime: "2024/01/02 10:05:00",
306
308
  headline: "先ほどの地震情報を取り消します。",
307
309
  publishingOffice: "気象庁",
310
+ eventId: null,
308
311
  isTest: true,
309
312
  };
310
313
  const FALLBACK_EARTHQUAKE_ENCHI = {
@@ -314,6 +317,7 @@ const FALLBACK_EARTHQUAKE_ENCHI = {
314
317
  reportDateTime: "2024/01/03 08:20:00",
315
318
  headline: "日本への津波の影響はありません。",
316
319
  publishingOffice: "気象庁",
320
+ eventId: null,
317
321
  earthquake: {
318
322
  originTime: "2024/01/03 08:10:00",
319
323
  hypocenterName: "台湾付近",
@@ -332,6 +336,7 @@ const FALLBACK_EARTHQUAKE_SHINDO = {
332
336
  reportDateTime: "2024/01/04 14:00:00",
333
337
  headline: "各地の震度に関する情報です。",
334
338
  publishingOffice: "気象庁",
339
+ eventId: null,
335
340
  intensity: {
336
341
  maxInt: "5弱",
337
342
  areas: [
@@ -349,6 +354,7 @@ const FALLBACK_EARTHQUAKE_LG = {
349
354
  reportDateTime: "2024/01/05 19:30:00",
350
355
  headline: "関東地方で長周期地震動階級4を観測しました。",
351
356
  publishingOffice: "気象庁",
357
+ eventId: null,
352
358
  earthquake: {
353
359
  originTime: "2024/01/05 19:27:00",
354
360
  hypocenterName: "千葉県北西部",
package/dist/ui/theme.js CHANGED
@@ -189,6 +189,15 @@ exports.DEFAULT_ROLES = {
189
189
  // volcano: バナー
190
190
  volcanoAlertBanner: { bg: "vermillion", fg: "#FFFFFF", bold: true },
191
191
  volcanoFlashBanner: { bg: "darkRed", fg: "#FFFFFF", bold: true },
192
+ // stats: 統計表示
193
+ statsMuted: "gray",
194
+ statsCount: { fg: "sky", bold: true },
195
+ statsCategoryEew: { fg: "sky", bold: true },
196
+ statsCategoryEarthquake: { fg: "blue", bold: true },
197
+ statsCategoryTsunami: { fg: "blueGreen", bold: true },
198
+ statsCategoryVolcano: { fg: "orange", bold: true },
199
+ statsCategoryNankaiTrough: { fg: "vermillion", bold: true },
200
+ statsCategoryOther: { fg: "gray", bold: true },
192
201
  };
193
202
  /** ロール名の一覧 */
194
203
  const ROLE_NAMES = Object.keys(exports.DEFAULT_ROLES);