kokoirc 0.2.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/README.md +227 -0
  2. package/docs/commands/alias.md +42 -0
  3. package/docs/commands/ban.md +26 -0
  4. package/docs/commands/close.md +25 -0
  5. package/docs/commands/connect.md +26 -0
  6. package/docs/commands/deop.md +24 -0
  7. package/docs/commands/devoice.md +24 -0
  8. package/docs/commands/disconnect.md +26 -0
  9. package/docs/commands/help.md +28 -0
  10. package/docs/commands/ignore.md +47 -0
  11. package/docs/commands/items.md +95 -0
  12. package/docs/commands/join.md +25 -0
  13. package/docs/commands/kb.md +26 -0
  14. package/docs/commands/kick.md +25 -0
  15. package/docs/commands/log.md +82 -0
  16. package/docs/commands/me.md +24 -0
  17. package/docs/commands/mode.md +29 -0
  18. package/docs/commands/msg.md +26 -0
  19. package/docs/commands/nick.md +24 -0
  20. package/docs/commands/notice.md +24 -0
  21. package/docs/commands/op.md +24 -0
  22. package/docs/commands/part.md +25 -0
  23. package/docs/commands/quit.md +24 -0
  24. package/docs/commands/reload.md +19 -0
  25. package/docs/commands/script.md +126 -0
  26. package/docs/commands/server.md +61 -0
  27. package/docs/commands/set.md +37 -0
  28. package/docs/commands/topic.md +24 -0
  29. package/docs/commands/unalias.md +22 -0
  30. package/docs/commands/unban.md +25 -0
  31. package/docs/commands/unignore.md +25 -0
  32. package/docs/commands/voice.md +25 -0
  33. package/docs/commands/whois.md +24 -0
  34. package/docs/commands/wii.md +23 -0
  35. package/package.json +38 -0
  36. package/src/app/App.tsx +205 -0
  37. package/src/core/commands/docs.ts +183 -0
  38. package/src/core/commands/execution.ts +114 -0
  39. package/src/core/commands/help-formatter.ts +185 -0
  40. package/src/core/commands/helpers.ts +168 -0
  41. package/src/core/commands/index.ts +7 -0
  42. package/src/core/commands/parser.ts +33 -0
  43. package/src/core/commands/registry.ts +1394 -0
  44. package/src/core/commands/types.ts +19 -0
  45. package/src/core/config/defaults.ts +66 -0
  46. package/src/core/config/loader.ts +209 -0
  47. package/src/core/constants.ts +20 -0
  48. package/src/core/init.ts +32 -0
  49. package/src/core/irc/antiflood.ts +244 -0
  50. package/src/core/irc/client.ts +145 -0
  51. package/src/core/irc/events.ts +1031 -0
  52. package/src/core/irc/formatting.ts +132 -0
  53. package/src/core/irc/ignore.ts +84 -0
  54. package/src/core/irc/index.ts +2 -0
  55. package/src/core/irc/netsplit.ts +292 -0
  56. package/src/core/scripts/api.ts +240 -0
  57. package/src/core/scripts/event-bus.ts +82 -0
  58. package/src/core/scripts/index.ts +26 -0
  59. package/src/core/scripts/manager.ts +154 -0
  60. package/src/core/scripts/types.ts +256 -0
  61. package/src/core/state/selectors.ts +39 -0
  62. package/src/core/state/sorting.ts +30 -0
  63. package/src/core/state/store.ts +242 -0
  64. package/src/core/storage/crypto.ts +78 -0
  65. package/src/core/storage/db.ts +107 -0
  66. package/src/core/storage/index.ts +80 -0
  67. package/src/core/storage/query.ts +204 -0
  68. package/src/core/storage/types.ts +37 -0
  69. package/src/core/storage/writer.ts +130 -0
  70. package/src/core/theme/index.ts +3 -0
  71. package/src/core/theme/loader.ts +45 -0
  72. package/src/core/theme/parser.ts +518 -0
  73. package/src/core/theme/renderer.tsx +25 -0
  74. package/src/index.tsx +17 -0
  75. package/src/types/config.ts +126 -0
  76. package/src/types/index.ts +107 -0
  77. package/src/types/irc-framework.d.ts +569 -0
  78. package/src/types/theme.ts +37 -0
  79. package/src/ui/ErrorBoundary.tsx +42 -0
  80. package/src/ui/chat/ChatView.tsx +39 -0
  81. package/src/ui/chat/MessageLine.tsx +92 -0
  82. package/src/ui/hooks/useStatusbarColors.ts +23 -0
  83. package/src/ui/input/CommandInput.tsx +273 -0
  84. package/src/ui/layout/AppLayout.tsx +126 -0
  85. package/src/ui/layout/TopicBar.tsx +46 -0
  86. package/src/ui/sidebar/BufferList.tsx +55 -0
  87. package/src/ui/sidebar/NickList.tsx +96 -0
  88. package/src/ui/splash/SplashScreen.tsx +100 -0
  89. package/src/ui/statusbar/StatusLine.tsx +205 -0
  90. package/themes/.gitkeep +0 -0
  91. package/themes/default.theme +57 -0
  92. package/tsconfig.json +19 -0
@@ -0,0 +1,518 @@
1
+ import type { StyledSpan } from "@/types/theme"
2
+
3
+ /**
4
+ * Irssi-compatible color map.
5
+ * Lowercase = normal, uppercase = bright.
6
+ */
7
+ const COLOR_MAP: Record<string, string> = {
8
+ k: "#000000", K: "#555555",
9
+ r: "#aa0000", R: "#ff5555",
10
+ g: "#00aa00", G: "#55ff55",
11
+ y: "#aa5500", Y: "#ffff55",
12
+ b: "#0000aa", B: "#5555ff",
13
+ m: "#aa00aa", M: "#ff55ff",
14
+ c: "#00aaaa", C: "#55ffff",
15
+ w: "#aaaaaa", W: "#ffffff",
16
+ }
17
+
18
+ /**
19
+ * Standard mIRC color palette (indices 0–98).
20
+ * 0–15: classic 16-color set, 16–98: extended palette.
21
+ */
22
+ const MIRC_COLORS: string[] = [
23
+ "#ffffff", "#000000", "#00007f", "#009300", "#ff0000", "#7f0000", "#9c009c", "#fc7f00",
24
+ "#ffff00", "#00fc00", "#009393", "#00ffff", "#0000fc", "#ff00ff", "#7f7f7f", "#d2d2d2",
25
+ "#470000", "#472100", "#474700", "#324700", "#004700", "#00472c", "#004747", "#002747",
26
+ "#000047", "#2e0047", "#470047", "#47002a", "#740000", "#743a00", "#747400", "#517400",
27
+ "#007400", "#007449", "#007474", "#004074", "#000074", "#4b0074", "#740074", "#740045",
28
+ "#b50000", "#b56300", "#b5b500", "#7db500", "#00b500", "#00b571", "#00b5b5", "#0063b5",
29
+ "#0000b5", "#7500b5", "#b500b5", "#b5006b", "#ff0000", "#ff8c00", "#ffff00", "#b2ff00",
30
+ "#00ff00", "#00ffa0", "#00ffff", "#008cff", "#0000ff", "#a500ff", "#ff00ff", "#ff0098",
31
+ "#ff5959", "#ffb459", "#ffff71", "#cfff60", "#6fff6f", "#65ffc9", "#6dffff", "#59b4ff",
32
+ "#5959ff", "#c459ff", "#ff66ff", "#ff59bc", "#ff9c9c", "#ffd39c", "#ffff9c", "#e2ff9c",
33
+ "#9cff9c", "#9cffdb", "#9cffff", "#9cd3ff", "#9c9cff", "#dc9cff", "#ff9cff", "#ff94d3",
34
+ "#000000", "#131313", "#282828", "#363636", "#4d4d4d", "#656565", "#818181", "#9f9f9f",
35
+ "#bcbcbc", "#e2e2e2", "#ffffff",
36
+ ]
37
+
38
+ const MAX_ABSTRACTION_DEPTH = 10
39
+
40
+ interface StyleState {
41
+ fg?: string
42
+ bg?: string
43
+ bold: boolean
44
+ italic: boolean
45
+ underline: boolean
46
+ dim: boolean
47
+ }
48
+
49
+ function defaultStyle(): StyleState {
50
+ return { bold: false, italic: false, underline: false, dim: false }
51
+ }
52
+
53
+ function cloneStyle(s: StyleState): StyleState {
54
+ return { ...s }
55
+ }
56
+
57
+ function styleToSpan(text: string, style: StyleState): StyledSpan {
58
+ const span: StyledSpan = {
59
+ text,
60
+ bold: style.bold,
61
+ italic: style.italic,
62
+ underline: style.underline,
63
+ dim: style.dim,
64
+ }
65
+ if (style.fg !== undefined) span.fg = style.fg
66
+ if (style.bg !== undefined) span.bg = style.bg
67
+ return span
68
+ }
69
+
70
+ /**
71
+ * Substitute positional variables ($0, $1, $*, $[N]0, $[-N]0) in a string.
72
+ */
73
+ function substituteVars(input: string, params: string[]): string {
74
+ let result = ""
75
+ let i = 0
76
+
77
+ while (i < input.length) {
78
+ if (input[i] === "$") {
79
+ i++
80
+ if (i >= input.length) {
81
+ result += "$"
82
+ break
83
+ }
84
+
85
+ // $* — all params joined with space
86
+ if (input[i] === "*") {
87
+ result += params.join(" ")
88
+ i++
89
+ continue
90
+ }
91
+
92
+ // $[N]D or $[-N]D — padded variable
93
+ if (input[i] === "[") {
94
+ i++ // skip [
95
+ let numStr = ""
96
+ while (i < input.length && input[i] !== "]") {
97
+ numStr += input[i]
98
+ i++
99
+ }
100
+ if (i < input.length) i++ // skip ]
101
+
102
+ const padWidth = parseInt(numStr, 10)
103
+ // Read the variable index digit(s)
104
+ let idxStr = ""
105
+ while (i < input.length && input[i] >= "0" && input[i] <= "9") {
106
+ idxStr += input[i]
107
+ i++
108
+ }
109
+ const idx = parseInt(idxStr, 10)
110
+ const value = idx < params.length ? params[idx] : ""
111
+
112
+ const absWidth = Math.abs(padWidth)
113
+ if (padWidth < 0) {
114
+ // left-pad
115
+ result += value.padStart(absWidth, " ")
116
+ } else {
117
+ // right-pad
118
+ result += value.padEnd(absWidth, " ")
119
+ }
120
+ continue
121
+ }
122
+
123
+ // $0, $1, ... $9 — positional variable (can be multi-digit)
124
+ if (input[i] >= "0" && input[i] <= "9") {
125
+ let idxStr = ""
126
+ while (i < input.length && input[i] >= "0" && input[i] <= "9") {
127
+ idxStr += input[i]
128
+ i++
129
+ }
130
+ const idx = parseInt(idxStr, 10)
131
+ result += idx < params.length ? params[idx] : ""
132
+ continue
133
+ }
134
+
135
+ // Not a recognized variable — keep the $ and current char
136
+ result += "$" + input[i]
137
+ i++
138
+ } else {
139
+ result += input[i]
140
+ i++
141
+ }
142
+ }
143
+
144
+ return result
145
+ }
146
+
147
+ /**
148
+ * Resolve `{name args...}` abstraction references.
149
+ * Recursively expands up to MAX_ABSTRACTION_DEPTH.
150
+ */
151
+ /**
152
+ * Find the matching closing brace, respecting nested braces.
153
+ */
154
+ function findMatchingBrace(input: string, openPos: number): number {
155
+ let depth = 1
156
+ let i = openPos + 1
157
+ while (i < input.length && depth > 0) {
158
+ if (input[i] === "{") depth++
159
+ else if (input[i] === "}") depth--
160
+ if (depth > 0) i++
161
+ }
162
+ return depth === 0 ? i : -1
163
+ }
164
+
165
+ /**
166
+ * Split abstraction args respecting nested braces.
167
+ * e.g., "$2 {pubnick $0}" → ["$2", "{pubnick $0}"]
168
+ */
169
+ function splitAbstractionArgs(argsStr: string): string[] {
170
+ const args: string[] = []
171
+ let current = ""
172
+ let depth = 0
173
+ for (let i = 0; i < argsStr.length; i++) {
174
+ if (argsStr[i] === "{") depth++
175
+ else if (argsStr[i] === "}") depth--
176
+
177
+ if (argsStr[i] === " " && depth === 0) {
178
+ if (current.length > 0) args.push(current)
179
+ current = ""
180
+ } else {
181
+ current += argsStr[i]
182
+ }
183
+ }
184
+ if (current.length > 0) args.push(current)
185
+ return args
186
+ }
187
+
188
+ export function resolveAbstractions(
189
+ input: string,
190
+ abstracts: Record<string, string>,
191
+ depth: number = 0,
192
+ ): string {
193
+ if (depth >= MAX_ABSTRACTION_DEPTH) return input
194
+
195
+ let result = ""
196
+ let i = 0
197
+
198
+ while (i < input.length) {
199
+ if (input[i] === "{") {
200
+ // Find matching closing brace (respecting nesting)
201
+ const closeIdx = findMatchingBrace(input, i)
202
+ if (closeIdx === -1) {
203
+ result += input[i]
204
+ i++
205
+ continue
206
+ }
207
+
208
+ const inner = input.substring(i + 1, closeIdx)
209
+ const spaceIdx = inner.indexOf(" ")
210
+
211
+ let name: string
212
+ let argsStr: string
213
+
214
+ if (spaceIdx === -1) {
215
+ name = inner
216
+ argsStr = ""
217
+ } else {
218
+ name = inner.substring(0, spaceIdx)
219
+ argsStr = inner.substring(spaceIdx + 1)
220
+ }
221
+
222
+ if (name in abstracts) {
223
+ const template = abstracts[name]
224
+ // Split args respecting nested braces
225
+ const args = argsStr ? splitAbstractionArgs(argsStr) : []
226
+ // First resolve nested abstractions in args
227
+ const resolvedArgs = args.map(a => resolveAbstractions(a, abstracts, depth + 1))
228
+ // Then substitute into template
229
+ const expanded = substituteVars(template, resolvedArgs)
230
+ // Recurse to handle any remaining abstractions
231
+ result += resolveAbstractions(expanded, abstracts, depth + 1)
232
+ } else {
233
+ result += input.substring(i, closeIdx + 1)
234
+ }
235
+
236
+ i = closeIdx + 1
237
+ } else {
238
+ result += input[i]
239
+ i++
240
+ }
241
+ }
242
+
243
+ return result
244
+ }
245
+
246
+ /**
247
+ * Parse an irssi-compatible format string into styled spans.
248
+ *
249
+ * Supports:
250
+ * - %X color codes (irssi color map)
251
+ * - %ZRRGGBB 24-bit hex colors
252
+ * - %_ %u %i %d style toggles (bold, underline, italic, dim)
253
+ * - %N/%n reset
254
+ * - %| indent marker (skipped)
255
+ * - $0 $1 $* $[N]0 $[-N]0 variable substitution
256
+ */
257
+ export function parseFormatString(input: string, params: string[] = []): StyledSpan[] {
258
+ // Step 1: Substitute variables
259
+ const text = substituteVars(input, params)
260
+
261
+ // Step 2: Walk char by char, parse color/style codes, build spans
262
+ const spans: StyledSpan[] = []
263
+ let current = defaultStyle()
264
+ let buffer = ""
265
+ let i = 0
266
+
267
+ function flush() {
268
+ if (buffer.length > 0) {
269
+ spans.push(styleToSpan(buffer, current))
270
+ buffer = ""
271
+ }
272
+ }
273
+
274
+ while (i < text.length) {
275
+ if (text[i] === "%") {
276
+ i++
277
+ if (i >= text.length) {
278
+ buffer += "%"
279
+ break
280
+ }
281
+
282
+ const code = text[i]
283
+
284
+ // %N or %n — reset all
285
+ if (code === "N" || code === "n") {
286
+ flush()
287
+ current = defaultStyle()
288
+ i++
289
+ continue
290
+ }
291
+
292
+ // %_ — toggle bold
293
+ if (code === "_") {
294
+ flush()
295
+ current = cloneStyle(current)
296
+ current.bold = !current.bold
297
+ i++
298
+ continue
299
+ }
300
+
301
+ // %u — toggle underline
302
+ if (code === "u") {
303
+ flush()
304
+ current = cloneStyle(current)
305
+ current.underline = !current.underline
306
+ i++
307
+ continue
308
+ }
309
+
310
+ // %i — toggle italic
311
+ if (code === "i") {
312
+ flush()
313
+ current = cloneStyle(current)
314
+ current.italic = !current.italic
315
+ i++
316
+ continue
317
+ }
318
+
319
+ // %d — toggle dim
320
+ if (code === "d") {
321
+ flush()
322
+ current = cloneStyle(current)
323
+ current.dim = !current.dim
324
+ i++
325
+ continue
326
+ }
327
+
328
+ // %Z — 24-bit hex color: %ZRRGGBB
329
+ if (code === "Z") {
330
+ flush()
331
+ const hex = text.substring(i + 1, i + 7)
332
+ current = cloneStyle(current)
333
+ current.fg = "#" + hex
334
+ i += 7
335
+ continue
336
+ }
337
+
338
+ // %| — indent marker, skip
339
+ if (code === "|") {
340
+ i++
341
+ continue
342
+ }
343
+
344
+ // Color code from color map
345
+ if (code in COLOR_MAP) {
346
+ flush()
347
+ current = cloneStyle(current)
348
+ current.fg = COLOR_MAP[code]
349
+ i++
350
+ continue
351
+ }
352
+
353
+ // %% — literal percent
354
+ if (code === "%") {
355
+ buffer += "%"
356
+ i++
357
+ continue
358
+ }
359
+
360
+ // Unknown code — keep as-is
361
+ buffer += "%" + code
362
+ i++
363
+ } else {
364
+ // ── mIRC control characters ──
365
+ const ch = text.charCodeAt(i)
366
+
367
+ // \x02 — bold toggle
368
+ if (ch === 0x02) {
369
+ flush()
370
+ current = cloneStyle(current)
371
+ current.bold = !current.bold
372
+ i++
373
+ continue
374
+ }
375
+
376
+ // \x03 — mIRC color: \x03[FG[,BG]]
377
+ if (ch === 0x03) {
378
+ flush()
379
+ current = cloneStyle(current)
380
+ i++
381
+
382
+ // Read up to 2 digits for foreground
383
+ let fgStr = ""
384
+ if (i < text.length && text[i] >= "0" && text[i] <= "9") {
385
+ fgStr += text[i]; i++
386
+ if (i < text.length && text[i] >= "0" && text[i] <= "9") {
387
+ fgStr += text[i]; i++
388
+ }
389
+ }
390
+
391
+ // Read optional ,BG (only if comma is followed by a digit)
392
+ let bgStr = ""
393
+ if (fgStr && i < text.length && text[i] === "," &&
394
+ i + 1 < text.length && text[i + 1] >= "0" && text[i + 1] <= "9") {
395
+ i++ // skip comma
396
+ bgStr += text[i]; i++
397
+ if (i < text.length && text[i] >= "0" && text[i] <= "9") {
398
+ bgStr += text[i]; i++
399
+ }
400
+ }
401
+
402
+ if (!fgStr) {
403
+ // \x03 alone = reset color
404
+ current.fg = undefined
405
+ current.bg = undefined
406
+ } else {
407
+ const fgNum = parseInt(fgStr, 10)
408
+ if (fgNum >= 0 && fgNum < MIRC_COLORS.length) {
409
+ current.fg = MIRC_COLORS[fgNum]
410
+ }
411
+ if (bgStr) {
412
+ const bgNum = parseInt(bgStr, 10)
413
+ if (bgNum >= 0 && bgNum < MIRC_COLORS.length) {
414
+ current.bg = MIRC_COLORS[bgNum]
415
+ }
416
+ }
417
+ }
418
+ continue
419
+ }
420
+
421
+ // \x04 — hex color: \x04[RRGGBB[,RRGGBB]]
422
+ if (ch === 0x04) {
423
+ flush()
424
+ current = cloneStyle(current)
425
+ i++
426
+
427
+ const fgHex = readHex6(text, i)
428
+ if (fgHex) {
429
+ current.fg = "#" + fgHex
430
+ i += 6
431
+ if (i < text.length && text[i] === ",") {
432
+ const bgHex = readHex6(text, i + 1)
433
+ if (bgHex) {
434
+ current.bg = "#" + bgHex
435
+ i += 7 // comma + 6 hex chars
436
+ }
437
+ }
438
+ } else {
439
+ // \x04 alone = reset color
440
+ current.fg = undefined
441
+ current.bg = undefined
442
+ }
443
+ continue
444
+ }
445
+
446
+ // \x0F — reset all formatting
447
+ if (ch === 0x0F) {
448
+ flush()
449
+ current = defaultStyle()
450
+ i++
451
+ continue
452
+ }
453
+
454
+ // \x16 — reverse (swap fg/bg)
455
+ if (ch === 0x16) {
456
+ flush()
457
+ current = cloneStyle(current)
458
+ const tmp = current.fg
459
+ current.fg = current.bg
460
+ current.bg = tmp
461
+ i++
462
+ continue
463
+ }
464
+
465
+ // \x1D — italic toggle
466
+ if (ch === 0x1D) {
467
+ flush()
468
+ current = cloneStyle(current)
469
+ current.italic = !current.italic
470
+ i++
471
+ continue
472
+ }
473
+
474
+ // \x1E — strikethrough toggle (mapped to dim — terminals lack strikethrough)
475
+ if (ch === 0x1E) {
476
+ flush()
477
+ current = cloneStyle(current)
478
+ current.dim = !current.dim
479
+ i++
480
+ continue
481
+ }
482
+
483
+ // \x1F — underline toggle
484
+ if (ch === 0x1F) {
485
+ flush()
486
+ current = cloneStyle(current)
487
+ current.underline = !current.underline
488
+ i++
489
+ continue
490
+ }
491
+
492
+ // \x11 — monospace (no-op in terminal)
493
+ if (ch === 0x11) {
494
+ i++
495
+ continue
496
+ }
497
+
498
+ buffer += text[i]
499
+ i++
500
+ }
501
+ }
502
+
503
+ flush()
504
+
505
+ // If nothing was produced (empty input), return a single empty span
506
+ if (spans.length === 0) {
507
+ spans.push(styleToSpan("", defaultStyle()))
508
+ }
509
+
510
+ return spans
511
+ }
512
+
513
+ /** Read exactly 6 hex chars from position, or return null. */
514
+ function readHex6(text: string, pos: number): string | null {
515
+ if (pos + 6 > text.length) return null
516
+ const slice = text.substring(pos, pos + 6)
517
+ return /^[0-9a-fA-F]{6}$/.test(slice) ? slice : null
518
+ }
@@ -0,0 +1,25 @@
1
+ import { TextAttributes } from "@opentui/core"
2
+ import type { StyledSpan } from "@/types/theme"
3
+
4
+ interface Props {
5
+ spans: StyledSpan[]
6
+ }
7
+
8
+ export function StyledText({ spans }: Props) {
9
+ return (
10
+ <text>
11
+ {spans.map((span, i) => {
12
+ let content: any = span.text
13
+ if (span.bold) content = <strong>{content}</strong>
14
+ if (span.italic) content = <em>{content}</em>
15
+ if (span.underline) content = <u>{content}</u>
16
+ if (span.dim) content = <span attributes={TextAttributes.DIM}>{content}</span>
17
+ return (
18
+ <span key={i} fg={span.fg} bg={span.bg}>
19
+ {content}
20
+ </span>
21
+ )
22
+ })}
23
+ </text>
24
+ )
25
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,17 @@
1
+ import { createCliRenderer } from "@opentui/core"
2
+ import { createRoot } from "@opentui/react"
3
+ import { App } from "./app/App"
4
+ import { ErrorBoundary } from "./ui/ErrorBoundary"
5
+
6
+ const renderer = await createCliRenderer({
7
+ exitOnCtrlC: true,
8
+ autoFocus: true,
9
+ useMouse: true,
10
+ enableMouseMovement: true,
11
+ })
12
+
13
+ createRoot(renderer).render(
14
+ <ErrorBoundary>
15
+ <App />
16
+ </ErrorBoundary>
17
+ )
@@ -0,0 +1,126 @@
1
+ import type { MessageType } from "@/types"
2
+
3
+ export type NickAlignment = 'left' | 'right' | 'center'
4
+
5
+ export type StatusbarItem = 'active_windows' | 'nick_info' | 'channel_info' | 'lag' | 'time'
6
+
7
+ export interface StatusbarConfig {
8
+ // Status line (info bar above input)
9
+ enabled: boolean
10
+ items: StatusbarItem[]
11
+ separator: string
12
+ item_formats: Record<string, string>
13
+
14
+ // Shared appearance for the whole bottom area (status + input)
15
+ background: string
16
+ text_color: string
17
+ accent_color: string
18
+ muted_color: string
19
+ dim_color: string
20
+
21
+ // Input line (prompt)
22
+ prompt: string // format: $channel, $nick, $buffer — substituted at render
23
+ prompt_color: string
24
+ input_color: string
25
+ cursor_color: string
26
+ }
27
+
28
+ export const DEFAULT_ITEM_FORMATS: Record<StatusbarItem, string> = {
29
+ active_windows: "Act: $activity",
30
+ nick_info: "$nick$modes",
31
+ channel_info: "$name$modes",
32
+ lag: "Lag: $lag",
33
+ time: "$time",
34
+ }
35
+
36
+ export type IgnoreLevel =
37
+ | 'MSGS' | 'PUBLIC' | 'NOTICES' | 'ACTIONS'
38
+ | 'JOINS' | 'PARTS' | 'QUITS' | 'NICKS' | 'KICKS'
39
+ | 'CTCPS' | 'ALL'
40
+
41
+ export const ALL_IGNORE_LEVELS: IgnoreLevel[] = [
42
+ 'MSGS', 'PUBLIC', 'NOTICES', 'ACTIONS',
43
+ 'JOINS', 'PARTS', 'QUITS', 'NICKS', 'KICKS',
44
+ 'CTCPS', 'ALL',
45
+ ]
46
+
47
+ export interface IgnoreEntry {
48
+ mask: string // nick or nick!user@host wildcard pattern
49
+ levels: IgnoreLevel[] // which event types to ignore
50
+ channels?: string[] // restrict to specific channels (empty = all)
51
+ }
52
+
53
+ export interface LoggingConfig {
54
+ enabled: boolean
55
+ encrypt: boolean
56
+ retention_days: number
57
+ exclude_types: MessageType[]
58
+ }
59
+
60
+ export interface ScriptsConfig {
61
+ autoload: string[]
62
+ debug: boolean
63
+ [scriptName: string]: any // per-script config: [scripts.my-script]
64
+ }
65
+
66
+ export interface AppConfig {
67
+ general: GeneralConfig
68
+ display: DisplayConfig
69
+ sidepanel: SidepanelConfig
70
+ statusbar: StatusbarConfig
71
+ servers: Record<string, ServerConfig>
72
+ aliases: Record<string, string>
73
+ ignores: IgnoreEntry[]
74
+ scripts: ScriptsConfig
75
+ logging: LoggingConfig
76
+ }
77
+
78
+ export interface GeneralConfig {
79
+ nick: string
80
+ username: string
81
+ realname: string
82
+ theme: string
83
+ timestamp_format: string
84
+ flood_protection: boolean
85
+ ctcp_version: string
86
+ }
87
+
88
+ export interface DisplayConfig {
89
+ nick_column_width: number
90
+ nick_max_length: number
91
+ nick_alignment: NickAlignment
92
+ nick_truncation: boolean
93
+ show_timestamps: boolean
94
+ scrollback_lines: number
95
+ }
96
+
97
+ export interface SidepanelConfig {
98
+ left: PanelConfig
99
+ right: PanelConfig
100
+ }
101
+
102
+ export interface PanelConfig {
103
+ width: number
104
+ visible: boolean
105
+ }
106
+
107
+ export interface ServerConfig {
108
+ label: string
109
+ address: string
110
+ port: number
111
+ tls: boolean
112
+ tls_verify: boolean
113
+ autoconnect: boolean
114
+ channels: string[]
115
+ nick?: string // per-server nick override (falls back to general.nick)
116
+ username?: string // per-server username override
117
+ realname?: string // per-server realname override
118
+ password?: string // server password (PASS command)
119
+ sasl_user?: string
120
+ sasl_pass?: string
121
+ bind_ip?: string // local IP to bind (vhost)
122
+ encoding?: string // character encoding (default: utf8)
123
+ auto_reconnect?: boolean // auto reconnect on disconnect (default: true)
124
+ reconnect_delay?: number // seconds between reconnect attempts (default: 30)
125
+ reconnect_max_retries?: number // max reconnect attempts (default: 10)
126
+ }