/**
* Full mIRC/IRCv3 colour table: 0-15 legacy + 16-98 extended (99 total).
*
* Codes 0-15 are the original mIRC palette used by essentially all IRC clients.
* Codes 16-98 are the modern IRCv3 extension published at
* https://modern.ircdocs.horse/formatting.html#color - supported by mIRC 7+,
* WeeChat, HexChat, and most modern clients.
*
* Each entry is a 0xAARRGGBB value.
*/
/**
* Canonical mIRC / IRCv3 colour palette — 99 entries (codes 0–98).
*
* Source: https://modern.ircdocs.horse/formatting.html#color (the "IRC Colour" specification).
* These are the exact RGB hex values that mIRC 7+, HexChat, WeeChat, and other modern
* clients use. Codes 0–15 are the original mIRC palette; codes 16–97 are the extended
* IRCv3 block (6 rows of 16, laid out as a gradient grid); code 98 is the spec-defined
* "transparent/default" entry which maps to white for rendering purposes.
*
* Layout of codes 16–97 in the grid (each row = 16 entries, darkest → lightest):
* Row 1 (16–27): greys darkening right → pure blacks at left
* Row 2 (28–39): dark shades — red, orange, yellow, green, cyan, blue, purple, pink
* Row 3 (40–51): mid shades
* Row 4 (52–63): bright / saturated
* Row 5 (64–75): light / pastel
* Row 6 (76–87): very light / near-white pastels
* Row 7 (88–98): greyscale ramp (black → white), code 98 = white alias
*/
private val MIRC_PALETTE: IntArray = intArrayOf(
// ── 0–15: classic mIRC palette ────────────────────────────────────────────
0xFFFFFFFF.toInt(), // 0 White
0xFF000000.toInt(), // 1 Black
0xFF00007F.toInt(), // 2 Blue (navy)
0xFF009300.toInt(), // 3 Green
0xFFFF0000.toInt(), // 4 Red
0xFF7F0000.toInt(), // 5 Brown (maroon)
0xFF9C009C.toInt(), // 6 Purple
0xFFFC7F00.toInt(), // 7 Orange
0xFFFFFF00.toInt(), // 8 Yellow
0xFF00FC00.toInt(), // 9 Light green (lime)
0xFF009393.toInt(), // 10 Teal
0xFF00FFFF.toInt(), // 11 Light cyan (aqua)
0xFF0000FC.toInt(), // 12 Light blue (royal)
0xFFFF00FF.toInt(), // 13 Pink (magenta / fuchsia)
0xFF7F7F7F.toInt(), // 14 Grey
0xFFD2D2D2.toInt(), // 15 Light grey
// ── 16–27: darkest shades (row 1 of extended block) ──────────────────────
0xFF470000.toInt(), // 16 Dark maroon
0xFF472100.toInt(), // 17 Very dark orange
0xFF474700.toInt(), // 18 Dark olive
0xFF324700.toInt(), // 19 Very dark green
0xFF004732.toInt(), // 20 Very dark teal-green
0xFF00472C.toInt(), // 21 Very dark teal (alt)
0xFF004747.toInt(), // 22 Very dark teal
0xFF002747.toInt(), // 23 Very dark slate blue
0xFF000047.toInt(), // 24 Very dark navy
0xFF2E0047.toInt(), // 25 Very dark violet
0xFF470047.toInt(), // 26 Very dark purple-magenta
0xFF47002A.toInt(), // 27 Very dark crimson
// ── 28–39: dark shades (row 2) ───────────────────────────────────────────
0xFF740000.toInt(), // 28 Dark red
0xFF743A00.toInt(), // 29 Dark brown-orange
0xFF747400.toInt(), // 30 Dark yellow-olive
0xFF517400.toInt(), // 31 Dark chartreuse
0xFF007400.toInt(), // 32 Dark green
0xFF007449.toInt(), // 33 Dark sea-green
0xFF007474.toInt(), // 34 Dark teal
0xFF004074.toInt(), // 35 Dark dodger blue
0xFF000074.toInt(), // 36 Dark blue
0xFF4B0074.toInt(), // 37 Dark purple-blue
0xFF740074.toInt(), // 38 Dark magenta
0xFF740045.toInt(), // 39 Dark hot pink
// ── 40–51: mid shades (row 3) ────────────────────────────────────────────
0xFFB50000.toInt(), // 40 Medium red
0xFFB56300.toInt(), // 41 Medium orange
0xFFB5B500.toInt(), // 42 Medium yellow-green
0xFF7DB500.toInt(), // 43 Chartreuse
0xFF00B500.toInt(), // 44 Medium green
0xFF00B573.toInt(), // 45 Medium mint
0xFF00B5B5.toInt(), // 46 Medium teal
0xFF0063B5.toInt(), // 47 Medium dodger blue
0xFF0000B5.toInt(), // 48 Medium blue
0xFF7500B5.toInt(), // 49 Medium violet
0xFFB500B5.toInt(), // 50 Medium magenta
0xFFB5006B.toInt(), // 51 Medium hot pink
// ── 52–63: bright / saturated (row 4) ────────────────────────────────────
0xFFFF0000.toInt(), // 52 Bright red
0xFFFF9200.toInt(), // 53 Bright orange / gold
0xFFFFFF00.toInt(), // 54 Bright yellow
0xFFB9FF00.toInt(), // 55 Bright yellow-green
0xFF00FF00.toInt(), // 56 Bright lime green
0xFF00FFA8.toInt(), // 57 Bright spring green
0xFF00FFFF.toInt(), // 58 Bright cyan / aqua
0xFF009BFF.toInt(), // 59 Bright azure / sky blue
0xFF0000FF.toInt(), // 60 Bright blue
0xFFAD00FF.toInt(), // 61 Bright electric purple
0xFFFF00FF.toInt(), // 62 Bright magenta / fuchsia
0xFFFF0092.toInt(), // 63 Bright rose / hot pink
// ── 64–75: light / pastel (row 5) ────────────────────────────────────────
0xFFFF6666.toInt(), // 64 Light red
0xFFFFB466.toInt(), // 65 Light orange / peach
0xFFFFFF66.toInt(), // 66 Light yellow
0xFFCCFF66.toInt(), // 67 Light chartreuse
0xFF66FF66.toInt(), // 68 Light green
0xFF66FFB4.toInt(), // 69 Light mint
0xFF66FFFF.toInt(), // 70 Light cyan
0xFF66B4FF.toInt(), // 71 Light sky blue
0xFF6666FF.toInt(), // 72 Light blue-purple
0xFFCC66FF.toInt(), // 73 Light violet
0xFFFF66FF.toInt(), // 74 Light magenta / orchid
0xFFFF66B4.toInt(), // 75 Light pink
// ── 76–87: very light / near-white pastels (row 6) ───────────────────────
0xFFFFB4B4.toInt(), // 76 Very light red / salmon
0xFFFFDEB4.toInt(), // 77 Very light orange / bisque
0xFFFFFFB4.toInt(), // 78 Very light yellow / cream
0xFFE6FFB4.toInt(), // 79 Very light chartreuse / honeydew
0xFFB4FFB4.toInt(), // 80 Very light green / mint cream
0xFFB4FFE6.toInt(), // 81 Very light mint / azure-mint
0xFFB4FFFF.toInt(), // 82 Very light cyan / azure
0xFFB4DEFF.toInt(), // 83 Very light sky blue / alice blue
0xFFB4B4FF.toInt(), // 84 Very light lavender
0xFFDEB4FF.toInt(), // 85 Very light violet / lavender blush
0xFFFFB4FF.toInt(), // 86 Very light magenta / thistle
0xFFFFB4DE.toInt(), // 87 Very light pink / lavender rose
// ── 88–98: greyscale ramp (row 7) ────────────────────────────────────────
0xFF000000.toInt(), // 88 Black
0xFF141414.toInt(), // 89 Near-black
0xFF282828.toInt(), // 90 Very dark grey
0xFF3C3C3C.toInt(), // 91 Dark grey
0xFF505050.toInt(), // 92 Dark-mid grey
0xFF646464.toInt(), // 93 Mid grey
0xFF787878.toInt(), // 94 Mid-light grey
0xFF8C8C8C.toInt(), // 95 Light-mid grey
0xFFA0A0A0.toInt(), // 96 Light grey
0xFFB4B4B4.toInt(), // 97 Pale grey
0xFFC8C8C8.toInt(), // 98 Silver / near-white (spec "default" alias)
)
private fun mircColor
(code
: Int
): Color? =
MIRC_PALETTE.
getOrNull(code
)?.
let { Color(it.
toLong() and 0xFFFFFFFFL
) }
/** How many mIRC colour codes are defined (0-based, inclusive of 0). */
private const val MIRC_COLOR_COUNT = 99
private fun MircStyleState.toSpanStyle(): SpanStyle {
val fgCode = if (reverse) bg else fg
val bgCode = if (reverse) fg else bg
val fgColor
= fgCode
?.
let(::mircColor
) ?: Color.
Unspecified
val bgColor
= bgCode
?.
let(::mircColor
) ?: Color.
Unspecified
return SpanStyle(
color = fgColor,
background = bgColor,
fontWeight = if (bold) FontWeight.Bold else null,
fontStyle = if (italic) FontStyle.Italic else null,
textDecoration = if (underline) TextDecoration.Underline else null,
)
}
private fun parseMircRuns
(input
: String): List
<MircRun
> {
if (input.isEmpty()) return emptyList()
val out = mutableListOf<MircRun>()
val buf = StringBuilder()
val st = MircStyleState()
fun flush() {
if (buf.isNotEmpty()) {
out += MircRun(buf.toString(), st.snapshot())
buf.setLength(0)
}
}
fun parseOneOrTwoDigits(startIndex: Int): Pair<Int?, Int> {
var i = startIndex
if (i >= input.length || !input[i].isDigit()) return (null to i)
val first = input[i]
i++
if (i < input.length && input[i].isDigit()) {
val num = ("$first${input[i]}").toIntOrNull()
i++
return (num to i)
}
return (first.toString().toIntOrNull() to i)
}
var i = 0
while (i < input.length) {
when (val c = input[i]) {
'\u0003' -> { // colour
flush()
i++
val (fg, ni) = parseOneOrTwoDigits(i)
i = ni
if (fg == null) {
// \x03 alone resets colours.
st.fg = null
st.bg = null
} else {
st.fg = fg
// Optional ,bg
if (i < input.length && input[i] == ',') {
i++
val (bg, n2) = parseOneOrTwoDigits(i)
i = n2
st.bg = bg
}
}
}
'\u000F' -> { // reset
flush()
st.reset()
i++
}
'\u0002' -> { // bold
flush(); st.bold = !st.bold; i++
}
'\u001D' -> { // italic
flush(); st.italic = !st.italic; i++
}
'\u001F' -> { // underline
flush(); st.underline = !st.underline; i++
}
'\u0016' -> { // reverse
flush(); st.reverse = !st.reverse; i++
}
else -> {
// Drop other C0 controls (except common whitespace).
if (c.code < 0x20 && c != '\n' && c != '\t' && c != '\r') {
i++
} else {
buf.append(c)
i++
}
}
}
}
flush()
return out
}
private fun AnnotatedString.Builder.appendIrcStyledLinkified(
linkStyle: SpanStyle,
ansiColorsEnabled
: Boolean = false,
) {
val hasAnsi = ansiColorsEnabled && text.contains('\u001b')
val hasMirc = mircColorsEnabled && !hasAnsi &&
(text.contains('\u0003') || text.contains('\u0004') || text.contains('\u0002') ||
text.contains('\u001D') || text.contains('\u001F') || text.contains('\u0016'))
when {
hasAnsi -> {
val runs = parseAnsiRuns(text)
if (runs.isEmpty()) return
for (r in runs) {
if (r.style.hasAnyStyle()) {
withStyle(r.style.ansiToSpanStyle()) { appendLinkified(this, r.text, linkStyle) }
} else {
appendLinkified(this, r.text, linkStyle)
}
}
}
hasMirc -> {
val runs = parseMircRuns(text)
if (runs.isEmpty()) return
for (r in runs) {
if (r.style.hasAnyStyle()) {
withStyle(r.style.toSpanStyle()) { appendLinkified(this, r.text, linkStyle) }
} else {
appendLinkified(this, r.text, linkStyle)
}
}
}
else -> appendLinkified(this, stripIrcFormatting(text), linkStyle)
}
}
@Composable
private fun AnnotatedClickableText(
text: AnnotatedString,
style: androidx.compose.ui.text.TextStyle = LocalTextStyle.current,
maxLines: Int = Int.MAX_VALUE,
overflow
: TextOverflow
= TextOverflow.
Clip,
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
) {
var layout: TextLayoutResult? by remember { mutableStateOf(null) }
Text(
text = text,
style = style,
maxLines = maxLines,
overflow = overflow,
onTextLayout = {
layout = it
onTextLayout?.invoke(it)
},
modifier = modifier.pointerInput(text) {
val vc = viewConfiguration
awaitEachGesture {
// Don't consume gestures: allow selection (long-press/drag) to work.
val down = awaitFirstDown(requireUnconsumed = false)
val downPos = down.position
val downTime = down.uptimeMillis
val up = waitForUpOrCancellation() ?: return@awaitEachGesture
val dt = up.uptimeMillis - downTime
val dist = (up.position - downPos).getDistance()
// Treat only quick taps as clicks so selection gestures don't accidentally open links.
if (dt <= 200 && dist <= vc.touchSlop) {
val l = layout ?: return@awaitEachGesture
val offset = l.getOffsetForPosition(up.position)
val ann = text.getStringAnnotations(start = offset, end = offset).firstOrNull()
if (ann != null) onAnnotationClick(ann.tag, ann.item)
}
}
}
)
}
@Composable
private fun IrcLinkifiedText(
ansiColorsEnabled
: Boolean = false,
linkStyle: SpanStyle,
style: androidx.compose.ui.text.TextStyle = LocalTextStyle.current,
maxLines: Int = Int.MAX_VALUE,
overflow
: TextOverflow
= TextOverflow.
Clip,
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
) {
val annotated = remember(text, linkStyle, mircColorsEnabled, ansiColorsEnabled) {
buildAnnotatedString { appendIrcStyledLinkified(text, linkStyle, mircColorsEnabled, ansiColorsEnabled) }
}
AnnotatedClickableText(
text = annotated,
onAnnotationClick = onAnnotationClick,
modifier = modifier,
style = style,
maxLines = maxLines,
overflow = overflow,
onTextLayout = onTextLayout,
)
}
/**
* Returns true when a chat message line looks like it belongs to a bot-generated
* ASCII/ANSI art block rather than normal coloured conversation.
*/
// Examine stripped content for structural shape, plus ANSI SGR and mIRC colour
// codes as additional signals. Colour detection is intentionally kept loose
// here because the block-size gate (≥2 consecutive lines) does the real false-
// positive filtering — a single coloured line never triggers art rendering.
val plain = stripIrcFormatting(text)
if (plain.length < 3) return false
// ANSI SGR colour sequences ([…m): almost never appear in normal IRC chat,
// so even one is a strong signal. Other ANSI escapes ([3~ = Delete key,
// [A = cursor up) are NOT art — check explicitly for the SGR terminator 'm'.
var i = 0
while (i < text.length) {
if (text[i] == '' && i + 1 < text.length && text[i + 1] == '[') {
var j = i + 2
while (j < text.length && (text[j].isDigit() || text[j] == ';')) j++
if (j < text.length && text[j] == 'm') return true
i = j + 1
} else i++
}
// mIRC colour codes (): common in normal chat, so require ≥4 to reduce noise.
// False positives at this per-line level are acceptable because the ≥2 consecutive
// lines requirement in the block builder will catch them before rendering.
if (text.count { it == '' } >= 4) return true
// Signal 1: ≥2 leading spaces AND first non-space char is structural.
// Art bots indent for column alignment and their content opens with box/drawing
// characters. Prose continuation lines (word-wrapped bot output like weather
// forecasts) also indent but start with a letter or digit after the spaces.
if (plain.length >= 3 && plain[0] == ' ' && plain[1] == ' ') {
val firstNs = plain.trimStart()
if (firstNs.isNotEmpty() && !firstNs[0].isLetterOrDigit()) return true
}
// Signal 2: high structural-symbol density — ≥30 % of non-space chars are
// drawing characters, on a line long enough to rule out typos/emoji.
var artCount = 0
var alphaCount = 0
for (ch in plain) {
if (ch == ' ') continue
if (ch.isLetterOrDigit()) alphaCount++ else artCount++
}
val nonSpace = artCount + alphaCount
if (nonSpace >= 16 && artCount.toFloat() / nonSpace >= 0.30f) return true
// Signal 3: single-space indent + border character patterns.
if (plain.length >= 4 && plain[0] == ' ' && plain[1] != ' ') {
val c1 = plain[1]; val c2 = plain[2]
// 3a: border char followed immediately by space or another border char.
if (c1 in "|/\\_-=+[]" && (c2 == ' ' || c2 in "|/\\_-=+[]")) return true
// 3b: framed label — first AND last non-space chars are both border chars.
val lastNs = plain.trimEnd().lastOrNull()
if (lastNs != null && c1 in "|/\\_-=+[]" && lastNs in "|/\\_-=+[]") return true
}
return false
}
/**
* A heterogeneous list item for the chat LazyColumn.
*
* [Single] wraps one normal message.
* [Art] wraps an entire run of consecutive art lines as one item, so all
* lines are rendered inside a single [Column] with zero inter-line gaps.
* Keys use even/odd Long split to avoid collisions between the two types.
*/
private sealed class RawItem {
data class Single(val msg: UiMessage) : RawItem()
data class Art(val msgs: List<UiMessage>) : RawItem() // chronological
}
private sealed class DisplayItem {
data class Single(val msg: UiMessage) : DisplayItem() {
override val key
: Long = msg.
id * 2L
}
data class Art(
val msgs: List<UiMessage>, // chronological: oldest first
) : DisplayItem() {
override val key
: Long = msgs.
first().
id * 2L
+ 1L
}
}
/**
* Builds the [DisplayItem] list consumed by the chat [LazyColumn].
*
* Consecutive art-like messages (from any sender) are merged into a single
* [DisplayItem.Art] item so they render gap-free inside one [Column].
*
* Performance — two-phase approach:
*
* Phase 1 (block detection): scans [reversedMessages] with [looksLikeArt] and
* groups consecutive art lines into [RawArt] blocks. Pure string ops, no
* allocation beyond the result list. Cached by (n, newestId) so it only
* re-runs when a message is added or removed.
*
* Phase 2 (font sizing): measures each [RawArt] block to find the font size
* that fits the widest line. Results are stored in a [HashMap] keyed by
* (firstMsgId, blockSize) so that:
* - A new non-art message arriving → Phase 1 re-runs (fast), Phase 2
* re-iterates but every art block is a cache hit → zero [TextMeasurer]
* calls.
* - A new art message extending a block → cache key changes (blockSize++)
* → only that block is re-measured, all others are hits.
* - Layout width or font size changes → cache is cleared and all blocks
* are re-measured once.
*/
@Composable
private fun rememberDisplayItems(
reversedMessages: List<UiMessage>,
style: androidx.compose.ui.text.TextStyle,
): List<DisplayItem> {
val textMeasurer = rememberTextMeasurer()
val naturalSizeSp = style.fontSize.value.takeIf { !it.isNaN() && it > 0f } ?: 14f
// ── Phase 1: block detection — O(n) string ops, no measurement ───────────
val n = reversedMessages.size
val newestId = reversedMessages.firstOrNull()?.id ?: -1L
val rawItems: List<RawItem> = remember(n, newestId) {
if (reversedMessages.isEmpty()) return@remember emptyList()
val result = mutableListOf<RawItem>()
val artRun = mutableListOf<UiMessage>() // accumulated in reversed order
fun flushArtRun() {
if (artRun.size >= 2) result.add(RawItem.Art(artRun.asReversed().toList()))
else artRun.forEach { result.add(RawItem.Single(it)) }
artRun.clear()
}
for (msg in reversedMessages) {
if (msg.from != null && looksLikeArt(msg.text)) artRun.add(msg)
else { flushArtRun(); result.add(RawItem.Single(msg)) }
}
flushArtRun()
result
}
// ── Phase 2: font sizing — only measures blocks missing from cache ────────
// Key: (firstMsgId * MAX_BLOCK + blockSize) — uniquely identifies a block's
// content since IRC message lists are append-only and blocks only grow by
// having newer messages added at the end of the chronological order.
// Cache is cleared when layout dimensions change so all blocks are re-sized.
val fontSizeCache
= remember
{ HashMap
<Long, Float
>() }
val prevWidth = remember { mutableStateOf(availableWidthPx) }
val prevNatSp = remember { mutableStateOf(naturalSizeSp) }
if (prevWidth.value != availableWidthPx || prevNatSp.value != naturalSizeSp) {
fontSizeCache.clear()
prevWidth.value = availableWidthPx
prevNatSp.value = naturalSizeSp
}
fun fontSizeForBlock
(msgs
: List
<UiMessage
>): Float {
// (firstMsgId shifted left 17 bits) OR blockSize — collision-free for
// any realistic block size (<131072 lines) and message ID space.
val cacheKey = (msgs.first().id shl 17) or msgs.size.toLong()
fontSizeCache[cacheKey]?.let { return it }
// Not cached — run the binary search (same algorithm as rememberMotdFontSizeSp).
val plainLines = msgs.map { stripIrcFormatting(it.text) }.filter { it.isNotEmpty() }
val sp = if (plainLines.isEmpty() || availableWidthPx <= 0f) {
naturalSizeSp
} else {
val widestAtNatural = plainLines.maxOf { line ->
textMeasurer.measure(
text = line,
style = style.copy(fontSize = naturalSizeSp.sp),
constraints = Constraints(maxWidth = Int.MAX_VALUE),
maxLines = 1,
softWrap = false,
).size.width.toFloat()
}
if (widestAtNatural <= availableWidthPx) {
naturalSizeSp
} else {
var lo = minFontSp
var hi = naturalSizeSp
repeat(8) {
val mid = (lo + hi) / 2f
val widest = plainLines.maxOf { line ->
textMeasurer.measure(
text = line,
style = style.copy(fontSize = mid.sp),
constraints = Constraints(maxWidth = Int.MAX_VALUE),
maxLines = 1,
softWrap = false,
).size.width.toFloat()
}
if (widest <= availableWidthPx) lo = mid else hi = mid
}
lo
}
}
fontSizeCache[cacheKey] = sp
return sp
}
return rawItems.map { raw ->
when (raw) {
is RawItem.Single -> DisplayItem.Single(raw.msg)
is RawItem.Art -> DisplayItem.Art(raw.msgs, fontSizeForBlock(raw.msgs))
}
}
}
/**
* Computes the single font size (in sp) that makes the widest MOTD line fit within
* [availableWidthPx] at the given [style]. Every MOTD line must be rendered at this
* same size so that monospace ASCII art columns stay aligned.
*
* Returns [style]'s natural size when all lines already fit, or [minFontSp] as a floor.
*/
@Composable
private fun rememberMotdFontSizeSp(
motdLines: List<String>,
style: androidx.compose.ui.text.TextStyle,
val textMeasurer = rememberTextMeasurer()
val naturalSizeSp = style.fontSize.value.takeIf { !it.isNaN() && it > 0f } ?: 14f
// Strip IRC formatting from every line for measurement (formatting chars have no width).
val plainLines = remember(motdLines) { motdLines.map { stripIrcFormatting(it) } }
return remember(plainLines, availableWidthPx, naturalSizeSp) {
if (availableWidthPx <= 0f || plainLines.isEmpty()) return@remember naturalSizeSp
// Find the widest line at the natural font size.
val widestAtNatural = plainLines.maxOf { line ->
textMeasurer.measure(
text = line,
style = style.copy(fontSize = naturalSizeSp.sp),
constraints = Constraints(maxWidth = Int.MAX_VALUE),
maxLines = 1,
softWrap = false,
).size.width
}
if (widestAtNatural <= availableWidthPx) return@remember naturalSizeSp
// Binary-search a single shared size that fits even the widest line.
var lo = minFontSp
var hi = naturalSizeSp
repeat(8) {
val mid = (lo + hi) / 2f
val widest = plainLines.maxOf { line ->
textMeasurer.measure(
text = line,
style = style.copy(fontSize = mid.sp),
constraints = Constraints(maxWidth = Int.MAX_VALUE),
maxLines = 1,
softWrap = false,
).size.width
}
if (widest <= availableWidthPx) lo = mid else hi = mid
}
lo
}
}
/**
* Renders a single MOTD line at [fontSizeSp]. The caller is responsible for computing
* a shared font size across all MOTD lines (via [rememberMotdFontSizeSp]) so that
* monospace ASCII art columns remain aligned across lines of different lengths.
*/
@Composable
private fun MotdLine(
style: androidx.compose.ui.text.TextStyle,
ansiColorsEnabled
: Boolean = false,
linkStyle: SpanStyle,
) {
IrcLinkifiedText(
text = text,
mircColorsEnabled = mircColorsEnabled,
ansiColorsEnabled = ansiColorsEnabled,
linkStyle = linkStyle,
onAnnotationClick = onAnnotationClick,
style = style.copy(fontSize = fontSizeSp.sp),
maxLines = 1,
overflow
= TextOverflow.
Clip,
)
}