@sayue_ltr/fleq 1.50.0 → 1.51.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +47 -5
  3. package/dist/config.js +35 -2
  4. package/dist/dmdata/rest-client.js +58 -3
  5. package/dist/dmdata/telegram-parser.js +37 -59
  6. package/dist/dmdata/ws-client.js +49 -18
  7. package/dist/engine/cli/cli-run.js +71 -1
  8. package/dist/engine/cli/cli.js +12 -0
  9. package/dist/engine/filter/compile-filter.js +21 -0
  10. package/dist/engine/filter/compiler.js +188 -0
  11. package/dist/engine/filter/errors.js +41 -0
  12. package/dist/engine/filter/field-registry.js +78 -0
  13. package/dist/engine/filter/index.js +15 -0
  14. package/dist/engine/filter/parser.js +137 -0
  15. package/dist/engine/filter/rank-maps.js +34 -0
  16. package/dist/engine/filter/tokenizer.js +121 -0
  17. package/dist/engine/filter/type-checker.js +104 -0
  18. package/dist/engine/filter/types.js +2 -0
  19. package/dist/engine/filter-template/pipeline-controller.js +73 -0
  20. package/dist/engine/filter-template/pipeline.js +16 -0
  21. package/dist/engine/messages/display-callbacks.js +7 -0
  22. package/dist/engine/messages/message-router.js +114 -182
  23. package/dist/engine/messages/summary-tracker.js +106 -0
  24. package/dist/engine/messages/telegram-stats.js +103 -0
  25. package/dist/engine/messages/volcano-route-handler.js +122 -0
  26. package/dist/engine/monitor/monitor.js +51 -3
  27. package/dist/engine/monitor/shutdown.js +1 -0
  28. package/dist/engine/notification/notifier.js +16 -1
  29. package/dist/engine/notification/sound-player.js +193 -28
  30. package/dist/engine/presentation/diff-store.js +158 -0
  31. package/dist/engine/presentation/diff-types.js +2 -0
  32. package/dist/engine/presentation/events/from-earthquake.js +53 -0
  33. package/dist/engine/presentation/events/from-eew.js +72 -0
  34. package/dist/engine/presentation/events/from-lg-observation.js +58 -0
  35. package/dist/engine/presentation/events/from-nankai-trough.js +39 -0
  36. package/dist/engine/presentation/events/from-raw.js +35 -0
  37. package/dist/engine/presentation/events/from-seismic-text.js +37 -0
  38. package/dist/engine/presentation/events/from-tsunami.js +51 -0
  39. package/dist/engine/presentation/events/from-volcano.js +88 -0
  40. package/dist/engine/presentation/events/to-presentation-event.js +32 -0
  41. package/dist/engine/presentation/level-helpers.js +118 -0
  42. package/dist/engine/presentation/processors/process-earthquake.js +36 -0
  43. package/dist/engine/presentation/processors/process-eew.js +90 -0
  44. package/dist/engine/presentation/processors/process-lg-observation.js +30 -0
  45. package/dist/engine/presentation/processors/process-message.js +53 -0
  46. package/dist/engine/presentation/processors/process-nankai-trough.js +30 -0
  47. package/dist/engine/presentation/processors/process-raw.js +22 -0
  48. package/dist/engine/presentation/processors/process-seismic-text.js +30 -0
  49. package/dist/engine/presentation/processors/process-tsunami.js +42 -0
  50. package/dist/engine/presentation/processors/process-volcano.js +41 -0
  51. package/dist/engine/presentation/types.js +2 -0
  52. package/dist/engine/startup/config-resolver.js +2 -0
  53. package/dist/engine/template/compile-template.js +18 -0
  54. package/dist/engine/template/compiler.js +102 -0
  55. package/dist/engine/template/field-accessor.js +25 -0
  56. package/dist/engine/template/filters.js +94 -0
  57. package/dist/engine/template/index.js +5 -0
  58. package/dist/engine/template/parser.js +190 -0
  59. package/dist/engine/template/tokenizer.js +96 -0
  60. package/dist/engine/template/types.js +2 -0
  61. package/dist/types.js +2 -1
  62. package/dist/ui/display-adapter.js +60 -0
  63. package/dist/ui/earthquake-formatter.js +17 -5
  64. package/dist/ui/eew-formatter.js +25 -10
  65. package/dist/ui/formatter.js +67 -32
  66. package/dist/ui/minimap/grid-layout.js +91 -0
  67. package/dist/ui/minimap/index.js +16 -0
  68. package/dist/ui/minimap/minimap-renderer.js +277 -0
  69. package/dist/ui/minimap/pref-mapping.js +82 -0
  70. package/dist/ui/minimap/types.js +2 -0
  71. package/dist/ui/night-overlay.js +56 -0
  72. package/dist/ui/repl-handlers/command-definitions.js +320 -0
  73. package/dist/ui/repl-handlers/index.js +11 -0
  74. package/dist/ui/repl-handlers/info-handlers.js +577 -0
  75. package/dist/ui/repl-handlers/operation-handlers.js +233 -0
  76. package/dist/ui/repl-handlers/settings-handlers.js +923 -0
  77. package/dist/ui/repl-handlers/types.js +10 -0
  78. package/dist/ui/repl.js +81 -1752
  79. package/dist/ui/statistics-formatter.js +208 -0
  80. package/dist/ui/status-line.js +69 -0
  81. package/dist/ui/summary/index.js +5 -0
  82. package/dist/ui/summary/summary-line.js +18 -0
  83. package/dist/ui/summary/summary-model.js +31 -0
  84. package/dist/ui/summary/token-builders.js +317 -0
  85. package/dist/ui/summary/types.js +2 -0
  86. package/dist/ui/summary/width-fit.js +41 -0
  87. package/dist/ui/summary-interval-formatter.js +72 -0
  88. package/dist/ui/theme.js +34 -5
  89. package/dist/ui/tip-shuffler.js +81 -0
  90. package/dist/ui/volcano-formatter.js +15 -13
  91. package/dist/ui/waiting-tips.js +289 -249
  92. package/package.json +1 -1
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fitTokensToWidth = fitTokensToWidth;
4
+ const formatter_1 = require("../formatter");
5
+ const SEPARATOR = " ";
6
+ const SEPARATOR_WIDTH = 2;
7
+ function fitTokensToWidth(tokens, maxWidth) {
8
+ if (tokens.length === 0)
9
+ return "";
10
+ // Step 1: Check if all tokens fit at preferred width
11
+ const totalPreferred = tokens.reduce((sum, t) => sum + t.preferredWidth, 0);
12
+ const separatorTotal = SEPARATOR_WIDTH * Math.max(0, tokens.length - 1);
13
+ if (totalPreferred + separatorTotal <= maxWidth) {
14
+ return tokens.map((t) => t.text).join(SEPARATOR);
15
+ }
16
+ // Step 2: Drop tokens by priority (4 → 3 → 2)
17
+ let remaining = [...tokens];
18
+ for (const pri of [4, 3, 2]) {
19
+ const currentWidth = calcTotalWidth(remaining);
20
+ if (currentWidth <= maxWidth)
21
+ break;
22
+ remaining = remaining.filter((t) => !(t.priority === pri && t.dropMode === "drop"));
23
+ }
24
+ // Step 3: Shorten tokens if still too wide
25
+ if (calcTotalWidth(remaining) > maxWidth) {
26
+ remaining = remaining.map((t) => {
27
+ if (t.dropMode === "shorten" && t.shortText != null) {
28
+ return { ...t, text: t.shortText };
29
+ }
30
+ return t;
31
+ });
32
+ }
33
+ return remaining.map((t) => t.text).join(SEPARATOR);
34
+ }
35
+ function calcTotalWidth(tokens) {
36
+ if (tokens.length === 0)
37
+ return 0;
38
+ const textWidth = tokens.reduce((sum, t) => sum + (0, formatter_1.visualWidth)(t.text), 0);
39
+ const sepWidth = SEPARATOR_WIDTH * (tokens.length - 1);
40
+ return textWidth + sepWidth;
41
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildSparkline = buildSparkline;
7
+ exports.formatSummaryInterval = formatSummaryInterval;
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const summary_tracker_1 = require("../engine/messages/summary-tracker");
10
+ /** sparkline で使う8段階の文字 */
11
+ const SPARK_CHARS = "▁▂▃▄▅▆▇█";
12
+ /** ドメインの表示ラベル */
13
+ const DOMAIN_LABELS = {
14
+ eew: "EEW",
15
+ earthquake: "地震",
16
+ tsunami: "津波",
17
+ seismicText: "テキスト",
18
+ lgObservation: "長周期",
19
+ volcano: "火山",
20
+ nankaiTrough: "南海トラフ",
21
+ raw: "その他",
22
+ };
23
+ /**
24
+ * sparklineData (数値配列) から sparkline 文字列を生成する。
25
+ * 最大値に対する比率で8段階の文字を選択する。全部0なら ▁ の繰り返し。
26
+ */
27
+ function buildSparkline(data) {
28
+ const max = Math.max(...data);
29
+ if (max === 0) {
30
+ return SPARK_CHARS[0].repeat(data.length);
31
+ }
32
+ return data
33
+ .map((v) => {
34
+ const ratio = v / max;
35
+ const idx = Math.min(Math.round(ratio * (SPARK_CHARS.length - 1)), SPARK_CHARS.length - 1);
36
+ return SPARK_CHARS[idx];
37
+ })
38
+ .join("");
39
+ }
40
+ /**
41
+ * 要約行をフォーマットする。
42
+ * @param snapshot SummaryWindowTracker のスナップショット
43
+ * @param intervalMinutes 要約間隔(分)
44
+ * @param sparkline sparkline を含めるか
45
+ */
46
+ function formatSummaryInterval(snapshot, intervalMinutes, sparkline) {
47
+ const parts = [];
48
+ // ドメイン別件数
49
+ const domainParts = [];
50
+ for (const [domain, count] of Object.entries(snapshot.byDomain)) {
51
+ if (count > 0) {
52
+ const label = DOMAIN_LABELS[domain] ?? domain;
53
+ domainParts.push(`${label} ${count}件`);
54
+ }
55
+ }
56
+ // ヘッダ
57
+ const header = chalk_1.default.gray(`── ${intervalMinutes}分要約 ──`);
58
+ const domainStr = domainParts.length > 0
59
+ ? domainParts.join(chalk_1.default.gray(" | "))
60
+ : chalk_1.default.gray("受信なし");
61
+ // maxInt
62
+ const maxIntStr = snapshot.maxIntSeen != null
63
+ ? chalk_1.default.gray(` (最大${snapshot.maxIntSeen})`)
64
+ : "";
65
+ parts.push(`${header} ${domainStr}${maxIntStr}`);
66
+ // sparkline 行
67
+ if (sparkline) {
68
+ const sparkStr = buildSparkline(snapshot.sparklineData);
69
+ parts.push(chalk_1.default.gray("受信 ") + sparkStr + chalk_1.default.gray(` (${summary_tracker_1.WINDOW_MINUTES}分)`));
70
+ }
71
+ return parts.join("\n");
72
+ }
package/dist/ui/theme.js CHANGED
@@ -40,6 +40,8 @@ exports.DEFAULT_ROLES = exports.DEFAULT_PALETTE = void 0;
40
40
  exports.hexToRgb = hexToRgb;
41
41
  exports.rgbToHex = rgbToHex;
42
42
  exports.resolveTheme = resolveTheme;
43
+ exports.setNightMode = setNightMode;
44
+ exports.isNightMode = isNightMode;
43
45
  exports.getThemePath = getThemePath;
44
46
  exports.loadTheme = loadTheme;
45
47
  exports.loadThemeFromPath = loadThemeFromPath;
@@ -58,6 +60,7 @@ const fs = __importStar(require("fs"));
58
60
  const path = __importStar(require("path"));
59
61
  const chalk_1 = __importDefault(require("chalk"));
60
62
  const config_1 = require("../config");
63
+ const night_overlay_1 = require("./night-overlay");
61
64
  /** RoleStyleDef の型ガード */
62
65
  function isRoleStyleDef(value) {
63
66
  if (typeof value === "string")
@@ -352,7 +355,29 @@ function buildDefaultResolvedTheme() {
352
355
  return deepFreezeTheme(theme);
353
356
  }
354
357
  // ── モジュール状態 ──
355
- let currentTheme = buildDefaultResolvedTheme();
358
+ let baseTheme = buildDefaultResolvedTheme();
359
+ let nightModeEnabled = false;
360
+ let currentTheme = baseTheme;
361
+ // ── night mode ──
362
+ /** baseTheme に night overlay を適用して currentTheme を更新する */
363
+ function rebuildActiveTheme() {
364
+ if (nightModeEnabled) {
365
+ currentTheme = deepFreezeTheme((0, night_overlay_1.applyNightOverlay)(baseTheme));
366
+ }
367
+ else {
368
+ currentTheme = baseTheme;
369
+ }
370
+ chalkCache.clear();
371
+ }
372
+ /** ナイトモードの ON/OFF を設定し、テーマを再構築する */
373
+ function setNightMode(enabled) {
374
+ nightModeEnabled = enabled;
375
+ rebuildActiveTheme();
376
+ }
377
+ /** ナイトモードが有効かどうかを返す */
378
+ function isNightMode() {
379
+ return nightModeEnabled;
380
+ }
356
381
  // ── パス解決 ──
357
382
  /** theme.json のパスを返す */
358
383
  function getThemePath() {
@@ -367,14 +392,16 @@ function loadTheme() {
367
392
  function loadThemeFromPath(themePath) {
368
393
  chalkCache.clear();
369
394
  if (!fs.existsSync(themePath)) {
370
- currentTheme = buildDefaultResolvedTheme();
395
+ baseTheme = buildDefaultResolvedTheme();
396
+ rebuildActiveTheme();
371
397
  return [];
372
398
  }
373
399
  try {
374
400
  const raw = fs.readFileSync(themePath, "utf-8");
375
401
  const parsed = JSON.parse(raw);
376
402
  if (typeof parsed !== "object" || parsed == null || Array.isArray(parsed)) {
377
- currentTheme = buildDefaultResolvedTheme();
403
+ baseTheme = buildDefaultResolvedTheme();
404
+ rebuildActiveTheme();
378
405
  return ["theme.json の形式が不正です。デフォルトテーマを使用します。"];
379
406
  }
380
407
  const { themeFile, warnings: sanitizeWarnings } = sanitizeThemeInput(parsed);
@@ -382,11 +409,13 @@ function loadThemeFromPath(themePath) {
382
409
  palette: exports.DEFAULT_PALETTE,
383
410
  roles: exports.DEFAULT_ROLES,
384
411
  });
385
- currentTheme = deepFreezeTheme(theme);
412
+ baseTheme = deepFreezeTheme(theme);
413
+ rebuildActiveTheme();
386
414
  return [...sanitizeWarnings, ...warnings];
387
415
  }
388
416
  catch (err) {
389
- currentTheme = buildDefaultResolvedTheme();
417
+ baseTheme = buildDefaultResolvedTheme();
418
+ rebuildActiveTheme();
390
419
  if (err instanceof SyntaxError) {
391
420
  return ["theme.json のJSONパースに失敗しました。デフォルトテーマを使用します。"];
392
421
  }
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TipShuffler = void 0;
4
+ const waiting_tips_1 = require("./waiting-tips");
5
+ /**
6
+ * 待機中Tipのエポックデッキ生成シャッフラ。
7
+ *
8
+ * - カテゴリごとにシャッフルした後、同カテゴリ連続を避けつつ
9
+ * 全Tipを1エポック分のデッキにインターリーブする。
10
+ * - デッキを使い切ったら自動で再構築する。
11
+ * - タイミング制御は持たず、`next()` で次のTipを返すだけの純粋な順序供給器。
12
+ */
13
+ class TipShuffler {
14
+ deck = [];
15
+ rng;
16
+ constructor(rng = Math.random) {
17
+ this.rng = rng;
18
+ this.rebuildDeck();
19
+ }
20
+ /** 次のTipを返す。デッキが空なら自動再構築。 */
21
+ next() {
22
+ if (this.deck.length === 0) {
23
+ this.rebuildDeck();
24
+ }
25
+ return this.deck.shift();
26
+ }
27
+ rebuildDeck() {
28
+ // 1. カテゴリごとにシャッフル
29
+ const buckets = [];
30
+ for (let ci = 0; ci < waiting_tips_1.TIP_CATEGORIES.length; ci++) {
31
+ const shuffled = this.shuffle([...waiting_tips_1.TIP_CATEGORIES[ci].tips]);
32
+ for (const tip of shuffled) {
33
+ buckets.push({ categoryIndex: ci, tip });
34
+ }
35
+ }
36
+ // 2. インターリーブ: 同カテゴリ連続を避けつつデッキ構築
37
+ this.deck = this.interleave(buckets);
38
+ }
39
+ /** 同カテゴリ連続を避けつつ全アイテムをインターリーブする */
40
+ interleave(items) {
41
+ // カテゴリごとのキューに分割
42
+ const queues = new Map();
43
+ for (const item of items) {
44
+ if (!queues.has(item.categoryIndex)) {
45
+ queues.set(item.categoryIndex, []);
46
+ }
47
+ queues.get(item.categoryIndex).push(item.tip);
48
+ }
49
+ const result = [];
50
+ let lastCategory = -1;
51
+ while (queues.size > 0) {
52
+ // 直前カテゴリ以外で残りがあるカテゴリから選択
53
+ const candidates = [...queues.keys()].filter((k) => k !== lastCategory);
54
+ if (candidates.length === 0) {
55
+ // 1カテゴリしか残っていない場合はそのまま流し込む
56
+ const remaining = [...queues.keys()][0];
57
+ result.push(...queues.get(remaining));
58
+ queues.delete(remaining);
59
+ break;
60
+ }
61
+ // ランダムに1カテゴリ選択
62
+ const chosen = candidates[Math.floor(this.rng() * candidates.length)];
63
+ const queue = queues.get(chosen);
64
+ result.push(queue.shift());
65
+ lastCategory = chosen;
66
+ if (queue.length === 0) {
67
+ queues.delete(chosen);
68
+ }
69
+ }
70
+ return result;
71
+ }
72
+ /** Fisher-Yates シャッフル */
73
+ shuffle(arr) {
74
+ for (let i = arr.length - 1; i > 0; i--) {
75
+ const j = Math.floor(this.rng() * (i + 1));
76
+ [arr[i], arr[j]] = [arr[j], arr[i]];
77
+ }
78
+ return arr;
79
+ }
80
+ }
81
+ exports.TipShuffler = TipShuffler;
@@ -86,13 +86,6 @@ function levelCodeToDisplay(code) {
86
86
  };
87
87
  return map[code] ?? code;
88
88
  }
89
- /** wrapFrameLines の結果を RenderBuffer に push */
90
- function pushWrapped(buf, level, content, width) {
91
- const lines = (0, formatter_1.wrapFrameLines)(level, content, width);
92
- for (const line of lines) {
93
- buf.push(line);
94
- }
95
- }
96
89
  // ── 火山本文ハイライト ──
97
90
  /** 火山本文キーワード強調ルール */
98
91
  const VOLCANO_HIGHLIGHT_RULES = [
@@ -211,20 +204,29 @@ function renderAlert(info, level, width, buf) {
211
204
  cardParts.push(`(${actionLabel(info.action)})`);
212
205
  buf.pushCard((0, formatter_1.frameLine)(level, ` ${cardParts.join(" ")}`, width));
213
206
  }
214
- // 対象市町村
207
+ // 対象市町村 (全件表示)
215
208
  if (info.municipalities.length > 0) {
216
209
  buf.push((0, formatter_1.frameDivider)(level, width));
217
210
  const muniNames = info.municipalities.map((m) => m.name);
218
- const maxMuni = (0, formatter_1.getTruncation)().volcanoMunicipalities;
219
- const muniLine = muniNames.slice(0, maxMuni).join(", ");
220
- const suffix = muniNames.length > maxMuni ? ` 他${muniNames.length - maxMuni}件` : "";
221
- pushWrapped(buf, level, ` ${chalk_1.default.gray("対象:")} ${muniLine}${suffix}`, width);
211
+ (0, formatter_1.renderSimpleNameList)({
212
+ level,
213
+ width,
214
+ items: muniNames,
215
+ label: "対象:",
216
+ buf,
217
+ });
222
218
  }
223
219
  // 対象海上予報区
224
220
  if (info.marineAreas.length > 0) {
225
221
  buf.push((0, formatter_1.frameDivider)(level, width));
226
222
  const areaNames = info.marineAreas.map((a) => a.name);
227
- pushWrapped(buf, level, ` ${chalk_1.default.gray("対象海域:")} ${areaNames.join(", ")}`, width);
223
+ (0, formatter_1.renderSimpleNameList)({
224
+ level,
225
+ width,
226
+ items: areaNames,
227
+ label: "対象海域:",
228
+ buf,
229
+ });
228
230
  }
229
231
  // 本文 (VolcanoActivity) / 防災事項 (VolcanoPrevention)
230
232
  if (info.bodyText) {