@pi-unipi/notify 2.6.1 → 2.9.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.
@@ -7,9 +7,9 @@
7
7
  */
8
8
 
9
9
  import type { Component } from "@earendil-works/pi-tui";
10
- import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
+ import { matchesKey } from "@earendil-works/pi-tui";
11
11
  import type { Theme } from "@earendil-works/pi-coding-agent";
12
- import { readModelCache, type CachedModel, boxInnerWidth } from "@pi-unipi/core";
12
+ import { readModelCache, type CachedModel, boxInnerWidth, OverlayTheme } from "@pi-unipi/core";
13
13
  import { loadConfig, saveConfig } from "../settings.js";
14
14
 
15
15
  const DEFAULT_MODEL = "openrouter/openai/gpt-oss-20b";
@@ -27,7 +27,7 @@ export class RecapModelSelectorOverlay implements Component {
27
27
  private error: string | null = null;
28
28
  onClose?: () => void;
29
29
  requestRender?: () => void;
30
- private theme: Theme | null = null;
30
+ private overlay = new OverlayTheme();
31
31
 
32
32
  /**
33
33
  * @param models Optional model list, preferably collected from Pi's live
@@ -51,7 +51,7 @@ export class RecapModelSelectorOverlay implements Component {
51
51
  }
52
52
 
53
53
  setTheme(theme: Theme): void {
54
- this.theme = theme;
54
+ this.overlay.setTheme(theme);
55
55
  }
56
56
 
57
57
  invalidate(): void {}
@@ -169,84 +169,49 @@ export class RecapModelSelectorOverlay implements Component {
169
169
  setTimeout(() => this.onClose?.(), 500);
170
170
  }
171
171
 
172
- // ─── Theme helpers ───────────────────────────────────────────────────
173
-
174
- private fg(color: string, text: string): string {
175
- if (this.theme) return this.theme.fg(color as any, text);
176
- const c: Record<string, string> = {
177
- accent: "\x1b[36m",
178
- success: "\x1b[32m",
179
- warning: "\x1b[33m",
180
- error: "\x1b[31m",
181
- dim: "\x1b[2m",
182
- borderMuted: "\x1b[90m",
183
- };
184
- return `${c[color] ?? ""}${text}\x1b[0m`;
185
- }
186
-
187
- private bold(text: string): string {
188
- return this.theme ? this.theme.bold(text) : `\x1b[1m${text}\x1b[0m`;
189
- }
190
-
191
- private frameLine(content: string, innerWidth: number): string {
192
- const truncated = truncateToWidth(content, innerWidth, "");
193
- const padding = Math.max(0, innerWidth - visibleWidth(truncated));
194
- return `${this.fg("borderMuted", "│")}${truncated}${" ".repeat(padding)}${this.fg("borderMuted", "│")}`;
195
- }
196
-
197
- private ruleLine(innerWidth: number): string {
198
- return this.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`);
199
- }
200
-
201
- private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
202
- const left = edge === "top" ? "┌" : "└";
203
- const right = edge === "top" ? "┐" : "┘";
204
- return this.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
205
- }
206
-
207
172
  render(width: number): string[] {
208
173
  const innerWidth = boxInnerWidth(width);
209
174
  const lines: string[] = [];
210
175
 
211
- lines.push(this.borderLine(innerWidth, "top"));
176
+ lines.push(this.overlay.borderLine(innerWidth, "top"));
212
177
  lines.push(
213
- this.frameLine(
214
- this.fg("accent", this.bold("🤖 Recap Model Selector")),
178
+ this.overlay.frameLine(
179
+ this.overlay.fg("accent", this.overlay.bold("🤖 Recap Model Selector")),
215
180
  innerWidth
216
181
  )
217
182
  );
218
183
  lines.push(
219
- this.frameLine(
220
- this.fg("dim", "Select model for notification recaps"),
184
+ this.overlay.frameLine(
185
+ this.overlay.fg("dim", "Select model for notification recaps"),
221
186
  innerWidth
222
187
  )
223
188
  );
224
- lines.push(this.ruleLine(innerWidth));
189
+ lines.push(this.overlay.ruleLine(innerWidth));
225
190
 
226
191
  // Filter bar
227
192
  if (this.filterMode) {
228
193
  lines.push(
229
- this.frameLine(
230
- ` ${this.fg("accent", "Filter:")} ${this.filter}${this.fg("accent", "█")}`,
194
+ this.overlay.frameLine(
195
+ ` ${this.overlay.fg("accent", "Filter:")} ${this.filter}${this.overlay.fg("accent", "█")}`,
231
196
  innerWidth
232
197
  )
233
198
  );
234
199
  } else if (this.filter) {
235
200
  lines.push(
236
- this.frameLine(
237
- ` ${this.fg("dim", "Filter:")} ${this.filter} ${this.fg("dim", "(press / to edit)")}`,
201
+ this.overlay.frameLine(
202
+ ` ${this.overlay.fg("dim", "Filter:")} ${this.filter} ${this.overlay.fg("dim", "(press / to edit)")}`,
238
203
  innerWidth
239
204
  )
240
205
  );
241
206
  } else {
242
207
  lines.push(
243
- this.frameLine(
244
- ` ${this.fg("dim", `/${this.models.length} models · press / to filter`)}`,
208
+ this.overlay.frameLine(
209
+ ` ${this.overlay.fg("dim", `/${this.models.length} models · press / to filter`)}`,
245
210
  innerWidth
246
211
  )
247
212
  );
248
213
  }
249
- lines.push(this.ruleLine(innerWidth));
214
+ lines.push(this.overlay.ruleLine(innerWidth));
250
215
 
251
216
  // Model list
252
217
  const terminalRows = process.stdout.rows ?? 30;
@@ -268,8 +233,8 @@ export class RecapModelSelectorOverlay implements Component {
268
233
  ? "No models — check ~/.pi/agent/models.json or API keys, then reopen"
269
234
  : "No models found";
270
235
  lines.push(
271
- this.frameLine(
272
- ` ${this.fg("dim", emptyMsg)}`,
236
+ this.overlay.frameLine(
237
+ ` ${this.overlay.fg("dim", emptyMsg)}`,
273
238
  innerWidth
274
239
  )
275
240
  );
@@ -277,20 +242,20 @@ export class RecapModelSelectorOverlay implements Component {
277
242
  for (let i = startIdx; i < endIdx; i++) {
278
243
  const m = this.filteredModels[i];
279
244
  const isSelected = i === this.selectedIndex;
280
- const marker = isSelected ? this.fg("accent", "▸") : " ";
245
+ const marker = isSelected ? this.overlay.fg("accent", "▸") : " ";
281
246
  const label = m.name || m.id;
282
247
  const fullRef = `${m.provider}/${m.id}`;
283
248
  const isDefault = fullRef === DEFAULT_MODEL;
284
249
  const defaultTag = isDefault
285
- ? ` ${this.fg("warning", "(default)")}`
250
+ ? ` ${this.overlay.fg("warning", "(default)")}`
286
251
  : "";
287
252
 
288
- const providerTag = this.fg("dim", `[${m.provider}]`);
253
+ const providerTag = this.overlay.fg("dim", `[${m.provider}]`);
289
254
  const display = isSelected
290
- ? `${providerTag} ${this.bold(label)}${defaultTag}`
291
- : `${providerTag} ${this.fg("dim", label)}${defaultTag}`;
255
+ ? `${providerTag} ${this.overlay.bold(label)}${defaultTag}`
256
+ : `${providerTag} ${this.overlay.fg("dim", label)}${defaultTag}`;
292
257
 
293
- lines.push(this.frameLine(` ${marker} ${display}`, innerWidth));
258
+ lines.push(this.overlay.frameLine(` ${marker} ${display}`, innerWidth));
294
259
  }
295
260
  }
296
261
 
@@ -300,8 +265,8 @@ export class RecapModelSelectorOverlay implements Component {
300
265
  ((this.selectedIndex + 1) / this.filteredModels.length) * 100
301
266
  );
302
267
  lines.push(
303
- this.frameLine(
304
- this.fg("dim", ` ${pct}% (${this.selectedIndex + 1}/${this.filteredModels.length})`),
268
+ this.overlay.frameLine(
269
+ this.overlay.fg("dim", ` ${pct}% (${this.selectedIndex + 1}/${this.filteredModels.length})`),
305
270
  innerWidth
306
271
  )
307
272
  );
@@ -309,33 +274,33 @@ export class RecapModelSelectorOverlay implements Component {
309
274
 
310
275
  // Status messages
311
276
  if (this.error) {
312
- lines.push(this.ruleLine(innerWidth));
277
+ lines.push(this.overlay.ruleLine(innerWidth));
313
278
  lines.push(
314
- this.frameLine(` ${this.fg("error", `⚠ ${this.error}`)}`, innerWidth)
279
+ this.overlay.frameLine(` ${this.overlay.fg("error", `⚠ ${this.error}`)}`, innerWidth)
315
280
  );
316
281
  }
317
282
  if (this.saved) {
318
- lines.push(this.ruleLine(innerWidth));
283
+ lines.push(this.overlay.ruleLine(innerWidth));
319
284
  lines.push(
320
- this.frameLine(
321
- ` ${this.fg("success", "✓ Model saved")}`,
285
+ this.overlay.frameLine(
286
+ ` ${this.overlay.fg("success", "✓ Model saved")}`,
322
287
  innerWidth
323
288
  )
324
289
  );
325
290
  }
326
291
 
327
292
  // Footer
328
- lines.push(this.ruleLine(innerWidth));
293
+ lines.push(this.overlay.ruleLine(innerWidth));
329
294
  lines.push(
330
- this.frameLine(
331
- this.fg(
295
+ this.overlay.frameLine(
296
+ this.overlay.fg(
332
297
  "dim",
333
298
  "↑↓ navigate · / filter · Enter select · Esc cancel"
334
299
  ),
335
300
  innerWidth
336
301
  )
337
302
  );
338
- lines.push(this.borderLine(innerWidth, "bottom"));
303
+ lines.push(this.overlay.borderLine(innerWidth, "bottom"));
339
304
 
340
305
  return lines;
341
306
  }
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { Component } from "@earendil-works/pi-tui";
9
- import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
+ import { matchesKey } from "@earendil-works/pi-tui";
10
10
  import type { Theme } from "@earendil-works/pi-coding-agent";
11
11
  import {
12
12
  loadConfig,
@@ -15,7 +15,7 @@ import {
15
15
  } from "../settings.js";
16
16
  import { loadNtfyConfig, saveNtfyConfig, getNtfyConfigScope } from "../ntfy-config.js";
17
17
  import type { NotifyConfig, NtfyConfig } from "../types.js";
18
- import { boxInnerWidth } from "@pi-unipi/core";
18
+ import { OverlayTheme, boxInnerWidth } from "@pi-unipi/core";
19
19
 
20
20
  /** Section types */
21
21
  type Section = "platforms" | "events" | "recap";
@@ -35,7 +35,7 @@ export class NotifySettingsOverlay implements Component {
35
35
  requestRender?: () => void;
36
36
  /** Called when user presses M in recap section to open model selector */
37
37
  onOpenModelSelector?: () => void;
38
- private theme: Theme | null = null;
38
+ private overlay = new OverlayTheme();
39
39
 
40
40
  constructor() {
41
41
  this.config = loadConfig();
@@ -45,7 +45,7 @@ export class NotifySettingsOverlay implements Component {
45
45
  }
46
46
 
47
47
  setTheme(theme: Theme): void {
48
- this.theme = theme;
48
+ this.overlay.setTheme(theme);
49
49
  }
50
50
 
51
51
  invalidate(): void {}
@@ -151,66 +151,30 @@ export class NotifySettingsOverlay implements Component {
151
151
  setTimeout(() => this.onClose?.(), 500);
152
152
  }
153
153
 
154
- // ─── Theme helpers ───────────────────────────────────────────────────
155
-
156
- private fg(color: string, text: string): string {
157
- if (this.theme) return this.theme.fg(color as any, text);
158
- const c: Record<string, string> = {
159
- accent: "\x1b[36m", success: "\x1b[32m", warning: "\x1b[33m",
160
- error: "\x1b[31m", dim: "\x1b[2m", borderMuted: "\x1b[90m",
161
- };
162
- return `${c[color] ?? ""}${text}\x1b[0m`;
163
- }
164
-
165
- private bold(text: string): string {
166
- return this.theme ? this.theme.bold(text) : `\x1b[1m${text}\x1b[0m`;
167
- }
168
-
169
- private frameLine(content: string, innerWidth: number): string {
170
- const truncated = truncateToWidth(content, innerWidth, "");
171
- const padding = Math.max(0, innerWidth - visibleWidth(truncated));
172
- return `${this.fg("borderMuted", "│")}${truncated}${" ".repeat(padding)}${this.fg("borderMuted", "│")}`;
173
- }
174
-
175
- private ruleLine(innerWidth: number): string {
176
- return this.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`);
177
- }
178
-
179
- private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
180
- const left = edge === "top" ? "┌" : "└";
181
- const right = edge === "top" ? "┐" : "┘";
182
- return this.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
183
- }
184
-
185
- private getDialogHeight(): number {
186
- const terminalRows = process.stdout.rows ?? 30;
187
- return Math.max(14, Math.min(24, Math.floor(terminalRows * 0.65)));
188
- }
189
-
190
154
  render(width: number): string[] {
191
155
  const innerWidth = boxInnerWidth(width);
192
156
  const lines: string[] = [];
193
157
 
194
- lines.push(this.borderLine(innerWidth, "top"));
195
- lines.push(this.frameLine(this.fg("accent", this.bold("🔔 Notify Settings")), innerWidth));
196
- lines.push(this.frameLine(this.fg("dim", "Configure notification platforms and events"), innerWidth));
197
- lines.push(this.ruleLine(innerWidth));
158
+ lines.push(this.overlay.borderLine(innerWidth, "top"));
159
+ lines.push(this.overlay.frameLine(this.overlay.fg("accent", this.overlay.bold("🔔 Notify Settings")), innerWidth));
160
+ lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Configure notification platforms and events"), innerWidth));
161
+ lines.push(this.overlay.ruleLine(innerWidth));
198
162
 
199
163
  // Section tabs
200
164
  const platformTab =
201
165
  this.section === "platforms"
202
- ? this.fg("accent", this.bold("[Platforms]"))
203
- : this.fg("dim", "Platforms");
166
+ ? this.overlay.fg("accent", this.overlay.bold("[Platforms]"))
167
+ : this.overlay.fg("dim", "Platforms");
204
168
  const eventsTab =
205
169
  this.section === "events"
206
- ? this.fg("accent", this.bold("[Events]"))
207
- : this.fg("dim", "Events");
170
+ ? this.overlay.fg("accent", this.overlay.bold("[Events]"))
171
+ : this.overlay.fg("dim", "Events");
208
172
  const recapTab =
209
173
  this.section === "recap"
210
- ? this.fg("accent", this.bold("[Recap]"))
211
- : this.fg("dim", "Recap");
212
- lines.push(this.frameLine(` ${platformTab} ${eventsTab} ${recapTab}`, innerWidth));
213
- lines.push(this.ruleLine(innerWidth));
174
+ ? this.overlay.fg("accent", this.overlay.bold("[Recap]"))
175
+ : this.overlay.fg("dim", "Recap");
176
+ lines.push(this.overlay.frameLine(` ${platformTab} ${eventsTab} ${recapTab}`, innerWidth));
177
+ lines.push(this.overlay.ruleLine(innerWidth));
214
178
 
215
179
  if (this.section === "platforms") {
216
180
  this.renderPlatforms(lines, innerWidth);
@@ -222,21 +186,21 @@ export class NotifySettingsOverlay implements Component {
222
186
 
223
187
  // Status messages
224
188
  if (this.error) {
225
- lines.push(this.ruleLine(innerWidth));
226
- lines.push(this.frameLine(` ${this.fg("error", `⚠ ${this.error}`)}`, innerWidth));
189
+ lines.push(this.overlay.ruleLine(innerWidth));
190
+ lines.push(this.overlay.frameLine(` ${this.overlay.fg("error", `⚠ ${this.error}`)}`, innerWidth));
227
191
  }
228
192
  if (this.saved) {
229
- lines.push(this.ruleLine(innerWidth));
230
- lines.push(this.frameLine(` ${this.fg("success", "✓ Settings saved")}`, innerWidth));
193
+ lines.push(this.overlay.ruleLine(innerWidth));
194
+ lines.push(this.overlay.frameLine(` ${this.overlay.fg("success", "✓ Settings saved")}`, innerWidth));
231
195
  }
232
196
 
233
197
  // Footer
234
- lines.push(this.ruleLine(innerWidth));
198
+ lines.push(this.overlay.ruleLine(innerWidth));
235
199
  const footerHint = this.section === "recap"
236
200
  ? "↑↓ navigate · Space toggle · M change model · Tab switch · Enter save · Esc cancel"
237
201
  : "↑↓ navigate · Space toggle · Tab switch · Enter save · Esc cancel";
238
- lines.push(this.frameLine(this.fg("dim", footerHint), innerWidth));
239
- lines.push(this.borderLine(innerWidth, "bottom"));
202
+ lines.push(this.overlay.frameLine(this.overlay.fg("dim", footerHint), innerWidth));
203
+ lines.push(this.overlay.borderLine(innerWidth, "bottom"));
240
204
 
241
205
  return lines;
242
206
  }
@@ -278,16 +242,16 @@ export class NotifySettingsOverlay implements Component {
278
242
  for (let i = 0; i < platforms.length; i++) {
279
243
  const p = platforms[i];
280
244
  const isSelected = i === this.selectedIndex;
281
- const toggleOn = this.fg("success", "●");
282
- const toggleOff = this.fg("dim", "○");
245
+ const toggleOn = this.overlay.fg("success", "●");
246
+ const toggleOff = this.overlay.fg("dim", "○");
283
247
  // ntfy enabled state comes from resolved ntfy.json, not config.json
284
248
  const isEnabled = p.key === "ntfy" ? this.ntfyConfig.enabled : this.config[p.key].enabled;
285
249
  const toggle = isEnabled ? toggleOn : toggleOff;
286
- const label = isSelected ? this.bold(p.label) : this.fg("dim", p.label);
250
+ const label = isSelected ? this.overlay.bold(p.label) : this.overlay.fg("dim", p.label);
287
251
 
288
252
  lines.push(
289
- this.frameLine(
290
- `${isSelected ? this.fg("accent", "▸") : " "} ${toggle} ${label} ${this.fg("dim", p.detail)}`,
253
+ this.overlay.frameLine(
254
+ `${isSelected ? this.overlay.fg("accent", "▸") : " "} ${toggle} ${label} ${this.overlay.fg("dim", p.detail)}`,
291
255
  innerWidth
292
256
  )
293
257
  );
@@ -298,17 +262,17 @@ export class NotifySettingsOverlay implements Component {
298
262
  const i = platforms.length;
299
263
  const isSelected = i === this.selectedIndex;
300
264
  const isEnabled = this.config.native.suppressWhenFocused === true;
301
- const toggleOn = this.fg("success", "●");
302
- const toggleOff = this.fg("dim", "○");
265
+ const toggleOn = this.overlay.fg("success", "●");
266
+ const toggleOff = this.overlay.fg("dim", "○");
303
267
  const toggle = isEnabled ? toggleOn : toggleOff;
304
268
  const label = isSelected
305
- ? this.bold("Suppress when focused")
306
- : this.fg("dim", "Suppress when focused");
307
- const detail = this.fg("dim", isEnabled ? "Windows only — terminal in foreground → skip" : "Windows only");
269
+ ? this.overlay.bold("Suppress when focused")
270
+ : this.overlay.fg("dim", "Suppress when focused");
271
+ const detail = this.overlay.fg("dim", isEnabled ? "Windows only — terminal in foreground → skip" : "Windows only");
308
272
 
309
273
  lines.push(
310
- this.frameLine(
311
- `${isSelected ? this.fg("accent", "▸") : " "} ${toggle} ${label} ${detail}`,
274
+ this.overlay.frameLine(
275
+ `${isSelected ? this.overlay.fg("accent", "▸") : " "} ${toggle} ${label} ${detail}`,
312
276
  innerWidth
313
277
  )
314
278
  );
@@ -321,14 +285,14 @@ export class NotifySettingsOverlay implements Component {
321
285
  for (let i = 0; i < events.length; i++) {
322
286
  const [key, cfg] = events[i];
323
287
  const isSelected = i === this.selectedIndex;
324
- const toggleOn = this.fg("success", "●");
325
- const toggleOff = this.fg("dim", "○");
288
+ const toggleOn = this.overlay.fg("success", "●");
289
+ const toggleOff = this.overlay.fg("dim", "○");
326
290
  const toggle = cfg.enabled ? toggleOn : toggleOff;
327
- const label = isSelected ? this.bold(key) : this.fg("dim", key);
291
+ const label = isSelected ? this.overlay.bold(key) : this.overlay.fg("dim", key);
328
292
 
329
293
  lines.push(
330
- this.frameLine(
331
- `${isSelected ? this.fg("accent", "▸") : " "} ${toggle} ${label}`,
294
+ this.overlay.frameLine(
295
+ `${isSelected ? this.overlay.fg("accent", "▸") : " "} ${toggle} ${label}`,
332
296
  innerWidth
333
297
  )
334
298
  );
@@ -338,27 +302,27 @@ export class NotifySettingsOverlay implements Component {
338
302
  private renderRecap(lines: string[], innerWidth: number): void {
339
303
  // Toggle
340
304
  const isSelected = this.selectedIndex === 0;
341
- const toggleOn = this.fg("success", "●");
342
- const toggleOff = this.fg("dim", "○");
305
+ const toggleOn = this.overlay.fg("success", "●");
306
+ const toggleOff = this.overlay.fg("dim", "○");
343
307
  const toggle = this.config.recap.enabled ? toggleOn : toggleOff;
344
308
  const label = isSelected
345
- ? this.bold("Enable Recap")
346
- : this.fg("dim", "Enable Recap");
309
+ ? this.overlay.bold("Enable Recap")
310
+ : this.overlay.fg("dim", "Enable Recap");
347
311
 
348
312
  lines.push(
349
- this.frameLine(
350
- `${isSelected ? this.fg("accent", "▸") : " "} ${toggle} ${label}`,
313
+ this.overlay.frameLine(
314
+ `${isSelected ? this.overlay.fg("accent", "▸") : " "} ${toggle} ${label}`,
351
315
  innerWidth
352
316
  )
353
317
  );
354
318
 
355
319
  // Current model display
356
320
  const modelRef = this.config.recap.model;
357
- const modelLabel = this.fg("dim", ` Model: ${modelRef}`);
358
- lines.push(this.frameLine(modelLabel, innerWidth));
321
+ const modelLabel = this.overlay.fg("dim", ` Model: ${modelRef}`);
322
+ lines.push(this.overlay.frameLine(modelLabel, innerWidth));
359
323
  lines.push(
360
- this.frameLine(
361
- this.fg("dim", " Press M to change model"),
324
+ this.overlay.frameLine(
325
+ this.overlay.fg("dim", " Press M to change model"),
362
326
  innerWidth
363
327
  )
364
328
  );
@@ -6,11 +6,11 @@
6
6
  */
7
7
 
8
8
  import type { Component } from "@earendil-works/pi-tui";
9
- import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
+ import { matchesKey } from "@earendil-works/pi-tui";
10
10
  import type { Theme } from "@earendil-works/pi-coding-agent";
11
11
  import { pollForChatId } from "../platforms/telegram.js";
12
12
  import { updateConfig } from "../settings.js";
13
- import { boxInnerWidth } from "@pi-unipi/core";
13
+ import { OverlayTheme, boxInnerWidth } from "@pi-unipi/core";
14
14
 
15
15
  type SetupPhase = "instructions" | "token" | "polling" | "success" | "error" | "timeout";
16
16
 
@@ -35,10 +35,10 @@ export class TelegramSetupOverlay implements Component {
35
35
  private pasteBuffer = "";
36
36
  onClose?: () => void;
37
37
  requestRender?: () => void;
38
- private theme: Theme | null = null;
38
+ private overlay = new OverlayTheme();
39
39
 
40
40
  setTheme(theme: Theme): void {
41
- this.theme = theme;
41
+ this.overlay.setTheme(theme);
42
42
  }
43
43
 
44
44
  invalidate(): void {}
@@ -193,110 +193,79 @@ export class TelegramSetupOverlay implements Component {
193
193
  }
194
194
  }
195
195
 
196
- // ─── Theme helpers ───────────────────────────────────────────────────
197
-
198
- private fg(color: string, text: string): string {
199
- if (this.theme) return this.theme.fg(color as any, text);
200
- const c: Record<string, string> = {
201
- accent: "\x1b[36m", success: "\x1b[32m", warning: "\x1b[33m",
202
- error: "\x1b[31m", dim: "\x1b[2m", borderMuted: "\x1b[90m",
203
- };
204
- return `${c[color] ?? ""}${text}\x1b[0m`;
205
- }
206
-
207
- private bold(text: string): string {
208
- return this.theme ? this.theme.bold(text) : `\x1b[1m${text}\x1b[0m`;
209
- }
210
-
211
- private frameLine(content: string, innerWidth: number): string {
212
- const truncated = truncateToWidth(content, innerWidth, "");
213
- const padding = Math.max(0, innerWidth - visibleWidth(truncated));
214
- return `${this.fg("borderMuted", "│")}${truncated}${" ".repeat(padding)}${this.fg("borderMuted", "│")}`;
215
- }
216
-
217
- private ruleLine(innerWidth: number): string {
218
- return this.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`);
219
- }
220
-
221
- private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
222
- const left = edge === "top" ? "┌" : "└";
223
- const right = edge === "top" ? "┐" : "┘";
224
- return this.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
225
- }
226
-
227
196
  render(width: number): string[] {
228
197
  const innerWidth = boxInnerWidth(width);
229
198
  const lines: string[] = [];
230
199
 
231
- lines.push(this.borderLine(innerWidth, "top"));
232
- lines.push(this.frameLine(this.fg("accent", this.bold("🤖 Telegram Bot Setup")), innerWidth));
233
- lines.push(this.ruleLine(innerWidth));
200
+ lines.push(this.overlay.borderLine(innerWidth, "top"));
201
+ lines.push(this.overlay.frameLine(this.overlay.fg("accent", this.overlay.bold("🤖 Telegram Bot Setup")), innerWidth));
202
+ lines.push(this.overlay.ruleLine(innerWidth));
234
203
 
235
204
  switch (this.phase) {
236
205
  case "instructions":
237
- lines.push(this.frameLine(this.fg("dim", "Set up Telegram notifications in 3 steps:"), innerWidth));
238
- lines.push(this.frameLine("", innerWidth));
239
- lines.push(this.frameLine(` ${this.bold("1.")} Open Telegram and message ${this.fg("accent", "@BotFather")}`, innerWidth));
240
- lines.push(this.frameLine(` Send /newbot and follow the prompts to create a bot`, innerWidth));
241
- lines.push(this.frameLine("", innerWidth));
242
- lines.push(this.frameLine(` ${this.bold("2.")} Copy the bot token from BotFather`, innerWidth));
243
- lines.push(this.frameLine("", innerWidth));
244
- lines.push(this.frameLine(` ${this.bold("3.")} Send any message to your new bot`, innerWidth));
245
- lines.push(this.frameLine(` (We'll detect your chat ID automatically)`, innerWidth));
246
- lines.push(this.ruleLine(innerWidth));
247
- lines.push(this.frameLine(this.fg("dim", "Press Enter to continue, Esc to cancel"), innerWidth));
206
+ lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Set up Telegram notifications in 3 steps:"), innerWidth));
207
+ lines.push(this.overlay.frameLine("", innerWidth));
208
+ lines.push(this.overlay.frameLine(` ${this.overlay.bold("1.")} Open Telegram and message ${this.overlay.fg("accent", "@BotFather")}`, innerWidth));
209
+ lines.push(this.overlay.frameLine(` Send /newbot and follow the prompts to create a bot`, innerWidth));
210
+ lines.push(this.overlay.frameLine("", innerWidth));
211
+ lines.push(this.overlay.frameLine(` ${this.overlay.bold("2.")} Copy the bot token from BotFather`, innerWidth));
212
+ lines.push(this.overlay.frameLine("", innerWidth));
213
+ lines.push(this.overlay.frameLine(` ${this.overlay.bold("3.")} Send any message to your new bot`, innerWidth));
214
+ lines.push(this.overlay.frameLine(` (We'll detect your chat ID automatically)`, innerWidth));
215
+ lines.push(this.overlay.ruleLine(innerWidth));
216
+ lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Press Enter to continue, Esc to cancel"), innerWidth));
248
217
  break;
249
218
 
250
219
  case "token":
251
- lines.push(this.frameLine(this.fg("dim", "Paste your bot token from BotFather:"), innerWidth));
252
- lines.push(this.frameLine("", innerWidth));
253
- const display = this.fg("accent", this.bold(this.botToken || " "));
254
- lines.push(this.frameLine(` ${display}${this.fg("dim", "█")}`, innerWidth));
255
- lines.push(this.frameLine("", innerWidth));
256
- lines.push(this.frameLine(this.fg("dim", "Enter to start polling · Esc to cancel"), innerWidth));
220
+ lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Paste your bot token from BotFather:"), innerWidth));
221
+ lines.push(this.overlay.frameLine("", innerWidth));
222
+ const display = this.overlay.fg("accent", this.overlay.bold(this.botToken || " "));
223
+ lines.push(this.overlay.frameLine(` ${display}${this.overlay.fg("dim", "█")}`, innerWidth));
224
+ lines.push(this.overlay.frameLine("", innerWidth));
225
+ lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Enter to start polling · Esc to cancel"), innerWidth));
257
226
  break;
258
227
 
259
228
  case "polling": {
260
229
  const frame = SPINNER_FRAMES[this.spinnerFrame] || "⠋";
261
230
  const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
262
231
  const remaining = Math.max(0, 300 - elapsed);
263
- lines.push(this.frameLine(` ${this.fg("accent", frame)} ${this.bold("Waiting for first message...")}`, innerWidth));
264
- lines.push(this.frameLine("", innerWidth));
265
- lines.push(this.frameLine(` ${this.fg("dim", "Send any message to your bot in Telegram")}`, innerWidth));
266
- lines.push(this.frameLine(` ${this.fg("dim", `Timeout: ${Math.floor(remaining / 60)}:${String(remaining % 60).padStart(2, "0")}`)}`, innerWidth));
267
- lines.push(this.ruleLine(innerWidth));
268
- lines.push(this.frameLine(this.fg("dim", "Esc to cancel"), innerWidth));
232
+ lines.push(this.overlay.frameLine(` ${this.overlay.fg("accent", frame)} ${this.overlay.bold("Waiting for first message...")}`, innerWidth));
233
+ lines.push(this.overlay.frameLine("", innerWidth));
234
+ lines.push(this.overlay.frameLine(` ${this.overlay.fg("dim", "Send any message to your bot in Telegram")}`, innerWidth));
235
+ lines.push(this.overlay.frameLine(` ${this.overlay.fg("dim", `Timeout: ${Math.floor(remaining / 60)}:${String(remaining % 60).padStart(2, "0")}`)}`, innerWidth));
236
+ lines.push(this.overlay.ruleLine(innerWidth));
237
+ lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Esc to cancel"), innerWidth));
269
238
  break;
270
239
  }
271
240
 
272
241
  case "success":
273
- lines.push(this.frameLine(` ${this.fg("success", "✓ Telegram bot configured!")}`, innerWidth));
274
- lines.push(this.frameLine("", innerWidth));
275
- lines.push(this.frameLine(` ${this.fg("dim", `Chat ID: ${this.chatId}`)}`, innerWidth));
276
- lines.push(this.frameLine(` ${this.fg("dim", "Notifications will be sent to this chat")}`, innerWidth));
277
- lines.push(this.ruleLine(innerWidth));
278
- lines.push(this.frameLine(this.fg("dim", "Press Enter to close"), innerWidth));
242
+ lines.push(this.overlay.frameLine(` ${this.overlay.fg("success", "✓ Telegram bot configured!")}`, innerWidth));
243
+ lines.push(this.overlay.frameLine("", innerWidth));
244
+ lines.push(this.overlay.frameLine(` ${this.overlay.fg("dim", `Chat ID: ${this.chatId}`)}`, innerWidth));
245
+ lines.push(this.overlay.frameLine(` ${this.overlay.fg("dim", "Notifications will be sent to this chat")}`, innerWidth));
246
+ lines.push(this.overlay.ruleLine(innerWidth));
247
+ lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Press Enter to close"), innerWidth));
279
248
  break;
280
249
 
281
250
  case "error":
282
- lines.push(this.frameLine(` ${this.fg("error", "✗ Setup failed")}`, innerWidth));
283
- lines.push(this.frameLine("", innerWidth));
284
- lines.push(this.frameLine(` ${this.fg("dim", this.error || "Unknown error")}`, innerWidth));
285
- lines.push(this.ruleLine(innerWidth));
286
- lines.push(this.frameLine(this.fg("dim", "Press Enter to close"), innerWidth));
251
+ lines.push(this.overlay.frameLine(` ${this.overlay.fg("error", "✗ Setup failed")}`, innerWidth));
252
+ lines.push(this.overlay.frameLine("", innerWidth));
253
+ lines.push(this.overlay.frameLine(` ${this.overlay.fg("dim", this.error || "Unknown error")}`, innerWidth));
254
+ lines.push(this.overlay.ruleLine(innerWidth));
255
+ lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Press Enter to close"), innerWidth));
287
256
  break;
288
257
 
289
258
  case "timeout":
290
- lines.push(this.frameLine(` ${this.fg("warning", "⏰ Timed out after 5 minutes")}`, innerWidth));
291
- lines.push(this.frameLine("", innerWidth));
292
- lines.push(this.frameLine(` ${this.fg("dim", "Make sure you sent a message to your bot in Telegram")}`, innerWidth));
293
- lines.push(this.frameLine(` ${this.fg("dim", "You can try again with /unipi:notify-set-tg")}`, innerWidth));
294
- lines.push(this.ruleLine(innerWidth));
295
- lines.push(this.frameLine(this.fg("dim", "Press Enter to close"), innerWidth));
259
+ lines.push(this.overlay.frameLine(` ${this.overlay.fg("warning", "⏰ Timed out after 5 minutes")}`, innerWidth));
260
+ lines.push(this.overlay.frameLine("", innerWidth));
261
+ lines.push(this.overlay.frameLine(` ${this.overlay.fg("dim", "Make sure you sent a message to your bot in Telegram")}`, innerWidth));
262
+ lines.push(this.overlay.frameLine(` ${this.overlay.fg("dim", "You can try again with /unipi:notify-set-tg")}`, innerWidth));
263
+ lines.push(this.overlay.ruleLine(innerWidth));
264
+ lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Press Enter to close"), innerWidth));
296
265
  break;
297
266
  }
298
267
 
299
- lines.push(this.borderLine(innerWidth, "bottom"));
268
+ lines.push(this.overlay.borderLine(innerWidth, "bottom"));
300
269
  return lines;
301
270
  }
302
271
  }
package/types.ts CHANGED
@@ -83,8 +83,6 @@ export interface NotifyConfig {
83
83
  gotify: GotifyConfig;
84
84
  /** Telegram settings */
85
85
  telegram: TelegramConfig;
86
- /** ntfy settings */
87
- ntfy: NtfyConfig;
88
86
  /** Recap summarization settings */
89
87
  recap: RecapConfig;
90
88
  }