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.
- package/README.md +227 -0
- package/docs/commands/alias.md +42 -0
- package/docs/commands/ban.md +26 -0
- package/docs/commands/close.md +25 -0
- package/docs/commands/connect.md +26 -0
- package/docs/commands/deop.md +24 -0
- package/docs/commands/devoice.md +24 -0
- package/docs/commands/disconnect.md +26 -0
- package/docs/commands/help.md +28 -0
- package/docs/commands/ignore.md +47 -0
- package/docs/commands/items.md +95 -0
- package/docs/commands/join.md +25 -0
- package/docs/commands/kb.md +26 -0
- package/docs/commands/kick.md +25 -0
- package/docs/commands/log.md +82 -0
- package/docs/commands/me.md +24 -0
- package/docs/commands/mode.md +29 -0
- package/docs/commands/msg.md +26 -0
- package/docs/commands/nick.md +24 -0
- package/docs/commands/notice.md +24 -0
- package/docs/commands/op.md +24 -0
- package/docs/commands/part.md +25 -0
- package/docs/commands/quit.md +24 -0
- package/docs/commands/reload.md +19 -0
- package/docs/commands/script.md +126 -0
- package/docs/commands/server.md +61 -0
- package/docs/commands/set.md +37 -0
- package/docs/commands/topic.md +24 -0
- package/docs/commands/unalias.md +22 -0
- package/docs/commands/unban.md +25 -0
- package/docs/commands/unignore.md +25 -0
- package/docs/commands/voice.md +25 -0
- package/docs/commands/whois.md +24 -0
- package/docs/commands/wii.md +23 -0
- package/package.json +38 -0
- package/src/app/App.tsx +205 -0
- package/src/core/commands/docs.ts +183 -0
- package/src/core/commands/execution.ts +114 -0
- package/src/core/commands/help-formatter.ts +185 -0
- package/src/core/commands/helpers.ts +168 -0
- package/src/core/commands/index.ts +7 -0
- package/src/core/commands/parser.ts +33 -0
- package/src/core/commands/registry.ts +1394 -0
- package/src/core/commands/types.ts +19 -0
- package/src/core/config/defaults.ts +66 -0
- package/src/core/config/loader.ts +209 -0
- package/src/core/constants.ts +20 -0
- package/src/core/init.ts +32 -0
- package/src/core/irc/antiflood.ts +244 -0
- package/src/core/irc/client.ts +145 -0
- package/src/core/irc/events.ts +1031 -0
- package/src/core/irc/formatting.ts +132 -0
- package/src/core/irc/ignore.ts +84 -0
- package/src/core/irc/index.ts +2 -0
- package/src/core/irc/netsplit.ts +292 -0
- package/src/core/scripts/api.ts +240 -0
- package/src/core/scripts/event-bus.ts +82 -0
- package/src/core/scripts/index.ts +26 -0
- package/src/core/scripts/manager.ts +154 -0
- package/src/core/scripts/types.ts +256 -0
- package/src/core/state/selectors.ts +39 -0
- package/src/core/state/sorting.ts +30 -0
- package/src/core/state/store.ts +242 -0
- package/src/core/storage/crypto.ts +78 -0
- package/src/core/storage/db.ts +107 -0
- package/src/core/storage/index.ts +80 -0
- package/src/core/storage/query.ts +204 -0
- package/src/core/storage/types.ts +37 -0
- package/src/core/storage/writer.ts +130 -0
- package/src/core/theme/index.ts +3 -0
- package/src/core/theme/loader.ts +45 -0
- package/src/core/theme/parser.ts +518 -0
- package/src/core/theme/renderer.tsx +25 -0
- package/src/index.tsx +17 -0
- package/src/types/config.ts +126 -0
- package/src/types/index.ts +107 -0
- package/src/types/irc-framework.d.ts +569 -0
- package/src/types/theme.ts +37 -0
- package/src/ui/ErrorBoundary.tsx +42 -0
- package/src/ui/chat/ChatView.tsx +39 -0
- package/src/ui/chat/MessageLine.tsx +92 -0
- package/src/ui/hooks/useStatusbarColors.ts +23 -0
- package/src/ui/input/CommandInput.tsx +273 -0
- package/src/ui/layout/AppLayout.tsx +126 -0
- package/src/ui/layout/TopicBar.tsx +46 -0
- package/src/ui/sidebar/BufferList.tsx +55 -0
- package/src/ui/sidebar/NickList.tsx +96 -0
- package/src/ui/splash/SplashScreen.tsx +100 -0
- package/src/ui/statusbar/StatusLine.tsx +205 -0
- package/themes/.gitkeep +0 -0
- package/themes/default.theme +57 -0
- 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
|
+
}
|