@sayue_ltr/fleq 1.51.0 → 2.0.0

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.
@@ -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);
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EEW_TIP_CATEGORIES = void 0;
4
+ // ── 緊急地震速報の技術 ──────────────────────────────────────
5
+ // ── 世界の地震早期警報 ──────────────────────────────────────
6
+ exports.EEW_TIP_CATEGORIES = [
7
+ // ── 緊急地震速報の技術 ──
8
+ {
9
+ id: "eew-technology",
10
+ tips: [
11
+ // --- 基本原理 ---
12
+ "Tip: 緊急地震速報のシステムは、気象庁と防災科研の合計約1,700か所の地震計のデータをサーバで集約し、地震検知から数秒で震源・マグニチュード・予測震度を自動計算します。",
13
+ "Tip: EEWの処理は地震波の検知から最短約2秒で第1報を出すことを目指しています。P波のわずか数秒のデータから瞬時に震源を推定する超高速処理です。",
14
+ "Tip: EEWのマグニチュード推定はP波初期の最大振幅から行われ、続報では全相の最大振幅も使って更新されます。観測点が増えるほど推定精度が上がります。",
15
+ "Tip: 予測震度の計算には震源からの距離と地盤増幅率を考慮した距離減衰式が使われます。各地の地盤データは事前に組み込まれています。",
16
+ "Tip: EEWの猶予時間は「震源からの距離 ÷ S波速度 − 情報伝達の遅延」で決まります。震源から離れるほど猶予は長くなりますが、震源直上ではほぼゼロです。",
17
+ // --- IPF法とPLUM法 ---
18
+ "Tip: IPF法(Integrated Particle Filter)は複数の観測点のデータを統合して震源とマグニチュードを逐次更新する手法で、2016年からEEWに導入されています。",
19
+ "Tip: PLUM法(Propagation of Local Undamped Motion)は観測された実際の揺れの強さからその周辺の揺れを直接予測する手法で、震源推定のミスに強い利点があります。",
20
+ "Tip: 現在のEEWはIPF法とPLUM法のハイブリッド方式で、同一イベントについて両手法の予測震度を比較し大きい方を採用します。",
21
+ // --- 技術的課題 ---
22
+ "Tip: 同時に複数の地震が発生すると、異なる地震の波形が混ざり合いEEWの震源・マグニチュード推定が狂うことがあります。この「同時発生問題」は重要な技術課題です。",
23
+ "Tip: EEWの精度は震源からの距離と深さに影響されます。内陸浅発の直下型地震ではS波の到達が速く、速報が間に合わない構造的限界があります。",
24
+ "Tip: 地震の破壊伝播は断層面上を秒速約2〜3kmで広がります。M9級の巨大地震では断層の破壊に3分以上かかり、その間もEEWは更新を続けます。",
25
+ "Tip: 深発地震ではEEWの震源深さの仮定が外れて震度推定が大きくずれたり、異常震域で予想外の地域が揺れたりする課題があります。",
26
+ "Tip: EEWの取消報は主に雷・振動ノイズなどの誤検知時に発表されますが、観測点が疎な地域では実際の地震でも取消となる場合があります。",
27
+ // --- 精度と活用 ---
28
+ "Tip: EEWの予測震度には±1階級程度の誤差があり得ます。続報が出るたびに精度は上がりますが、初報は速さ重視のため振れ幅が大きくなります。",
29
+ "Tip: EEWは「予測」が外れても出すことに意味があります。たとえ精度が完璧でなくても、数秒の警告で身を守る行動を取れる可能性が生まれます。",
30
+ "Tip: EEW予報の第1報は速さ重視で不確実性が高いですが、第2報・第3報と続報が出るたびに観測データが増えて精度が向上していきます。",
31
+ "Tip: 「高度利用者向け」のEEW予報は気象業務支援センターを通じてリアルタイムに配信され、鉄道の自動制動やエレベーターの自動停止に活用されています。",
32
+ "Tip: 気象庁のEEWシステムには長周期地震動の予測機能が2023年2月から組み込まれ、高層ビル向けの早期警報に活用されています。",
33
+ "Tip: EEWの「予報」は1点の観測でも発表されますが、「警報」は2点以上で地震波が確認された場合に限られます。これは雷などの誤検知を防ぐためです。",
34
+ "Tip: EEWの警報は最大震度5弱以上または最大長周期地震動階級3以上が予想される場合に発表されます。発表対象地域は震度4以上または長周期3以上の地域を含みます。",
35
+ // --- 海底観測の効果 ---
36
+ "Tip: 海底観測網(S-net・DONET・N-net)のデータがEEWに統合されたことで、海溝型地震のP波をより早く捉えられるようになり、検知や警報発表が数秒〜十数秒以上早まる場合があります。",
37
+ // --- 歴史 ---
38
+ "Tip: 新幹線の地震検知システムUrEDASは1992年に実用化され、EEWの技術的先駆けと言えるシステムです。後に簡略化版のコンパクトUrEDASも開発されました。",
39
+ "Tip: 一般向けEEWの提供は2007年10月から始まりました。それ以前は高度利用者向けの限定的な配信のみでした。",
40
+ "Tip: 2007年の新潟県中越沖地震(M6.8)は一般向けEEW開始前の試験運用中に発生し、速報の有効性を実証するとともに改善点も浮き彫りにしました。",
41
+ ],
42
+ },
43
+ // ── 世界の地震早期警報 ──
44
+ {
45
+ id: "eew-global",
46
+ tips: [
47
+ "Tip: メキシコのSASMEXは1991年に運用を開始した世界初の公共向け地震早期警報システムで、太平洋沿岸の地震からメキシコシティまでの距離を猶予時間に活かしています。",
48
+ "Tip: アメリカのShakeAlertは2019年にカリフォルニアで一般向け配信が始まり、2021年にオレゴン・ワシントン州にも拡大した地震早期警報システムです。",
49
+ "Tip: 台湾の交通部中央気象署はEEWシステムを運用しており、内陸の中規模地震では約10秒で解が得られ、PWS等を通じて携帯端末に配信されます。",
50
+ "Tip: 韓国気象庁は2015年からEEWの運用を開始し、2016年の慶州地震(M5.8)での課題を受けてシステムの高速化が進められました。",
51
+ "Tip: 中国地震局のEEWシステムは世界最大の人口をカバーする規模で整備が進んでおり、四川省成都の成都高新減災研究所(ICL)による民間EEWも有名です。",
52
+ "Tip: トルコでは2023年の大地震災害を機にEEWの研究・地域実装が加速しており、AFADを中心にイズミルなどで早期警報プロジェクトが進んでいます。",
53
+ "Tip: イタリアのINGVは地中海地域のEEW研究の中心で、PRESTo/ElarmSなど複数の手法を組み合わせたシステムの開発を行っています。",
54
+ "Tip: ルーマニアは中深発地震帯(ヴランチャ地震帯)からの距離を利用して首都ブカレストに数十秒の猶予時間を確保するEEWを運用しています。",
55
+ "Tip: 世界のEEWシステムは、日本の気象庁方式のような「地震パラメータ推定型」と、観測された揺れを直接伝える「ウェーブフロント型」に大別されます。",
56
+ "Tip: EEWの国際的な研究コミュニティでは、機械学習(深層学習)によるP波の自動検出やマグニチュード推定の高速化が活発に研究されています。",
57
+ "Tip: スマートフォンの加速度センサーをクラウドソーシングで利用するMyShake(カリフォルニア大学バークレー校)は、低コストで地震を検知する新しいアプローチです。",
58
+ "Tip: Googleは2020年からAndroid端末の加速度センサーで地震を検知し、世界各国にEEW的な警報を配信するAndroid Earthquake Alerts Systemを展開しています。",
59
+ "Tip: チリはM8超の巨大地震が頻発する地震大国で、CSN(国立地震センター)がEEWの研究・導入を進めています。",
60
+ "Tip: カナダのNRCanはブリティッシュコロンビア州で2024年にEEWの運用を開始し、ケベック・東オンタリオにも2025年に拡大しました。",
61
+ ],
62
+ },
63
+ ];