@@ -6,66 +6,167 @@ import android.text.Spannable
66import android.text.SpannableStringBuilder
77import android.text.style.BackgroundColorSpan
88import android.text.style.ForegroundColorSpan
9+ import android.text.style.StrikethroughSpan
910import android.text.style.StyleSpan
11+ import android.text.style.URLSpan
1012import android.text.style.UnderlineSpan
1113import java.util.regex.Pattern
1214
1315object AnsiColorParser {
14- private val ANSI_PATTERN = Pattern .compile(" \\ u001B\\ [([\\ d;]*)m" )
15-
16- private const val DEFAULT_FOREGROUND = Color .LTGRAY
17- private const val DEFAULT_BACKGROUND = Color .TRANSPARENT // Mặc định không có màu nền
1816
19- // Cấu trúc lưu trữ trạng thái định dạng hiện tại của luồng log
17+ // CSI: ESC [ params intermediate finalByte (SGR dùng finalByte = 'm', ngoài ra còn
18+ // cursor move/clear line/hide cursor... với finalByte khác -> bị bỏ qua không render)
19+ private val CSI_PATTERN = Pattern .compile(
20+ " \\ u001B\\ [([0-9;:?]*)([ -/]*)([@-~])"
21+ )
22+
23+ // OSC: ESC ] body (BEL | ESC \) — dùng cho hyperlink (8;;URL) và các OSC khác (vd 0;title)
24+ private val OSC_PATTERN = Pattern .compile(
25+ " \\ u001B\\ ]([^\\ u0007\\ u001B]*)(?:\\ u0007|\\ u001B\\\\ )"
26+ )
27+
28+ // Escape đơn dạng ESC + 1 ký tự không phải '[' hoặc ']' (vd chọn charset ESC(B, ESC)0...)
29+ private val SIMPLE_ESC_PATTERN = Pattern .compile(
30+ " \\ u001B[()#][A-Za-z0-9]|\\ u001B[=>]"
31+ )
32+
33+ private const val DEFAULT_FOREGROUND = Color .LTGRAY
34+ private const val DEFAULT_BACKGROUND = Color .TRANSPARENT
35+
36+ // Trạng thái định dạng hiện tại của luồng log (giữ nguyên xuyên suốt các lần parse)
2037 private var currentFgColor = DEFAULT_FOREGROUND
2138 private var currentBgColor = DEFAULT_BACKGROUND
2239 private var isBold = false
40+ private var isItalic = false
2341 private var isUnderline = false
42+ private var isStrikethrough = false
43+ private var isReverse = false
44+ private var isDim = false
2445
25- // Bảng 16 màu ANSI cơ bản (cho cả Foreground và Background)
2646 private val ANSI_16_COLORS = intArrayOf(
27- Color .BLACK , // 0 - Black
28- Color .RED , // 1 - Red
29- Color .GREEN , // 2 - Green
30- Color .rgb(200 , 150 , 0 ), // 3 - Yellow (Tối đi một chút để tránh chói mắt)
31- Color .BLUE , // 4 - Blue
32- Color .MAGENTA , // 5 - Magenta
33- Color .CYAN , // 6 - Cyan
34- Color .WHITE , // 7 - White
35- // 8 - 15: Bright Colors (Màu sáng)
36- Color .DKGRAY , // 8 - Bright Black (Dark Gray)
37- Color .rgb(255 , 85 , 85 ), // 9 - Bright Red
38- Color .rgb(85 , 255 , 85 ), // 10 - Bright Green
39- Color .rgb(255 , 255 , 85 ), // 11 - Bright Yellow
40- Color .rgb(85 , 85 , 255 ), // 12 - Bright Blue
41- Color .rgb(255 , 85 , 255 ), // 13 - Bright Magenta
42- Color .rgb(85 , 255 , 255 ), // 14 - Bright Cyan
43- Color .WHITE // 15 - Bright White
47+ Color .BLACK ,
48+ Color .RED ,
49+ Color .GREEN ,
50+ Color .rgb(200 , 150 , 0 ),
51+ Color .BLUE ,
52+ Color .MAGENTA ,
53+ Color .CYAN ,
54+ Color .WHITE ,
55+ Color .DKGRAY ,
56+ Color .rgb(255 , 85 , 85 ),
57+ Color .rgb(85 , 255 , 85 ),
58+ Color .rgb(255 , 255 , 85 ),
59+ Color .rgb(85 , 85 , 255 ),
60+ Color .rgb(255 , 85 , 255 ),
61+ Color .rgb(85 , 255 , 255 ),
62+ Color .WHITE
4463 )
4564
4665 fun parse (text : String? ): CharSequence {
4766 if (text == null ) return " "
67+
4868 val builder = SpannableStringBuilder ()
49- val matcher = ANSI_PATTERN .matcher(text)
50- var lastHeight = 0
69+ val plainBuffer = StringBuilder ()
70+
71+ var pendingLinkUrl: String? = null
72+ var pendingLinkStart = - 1
5173
52- while (matcher.find()) {
53- val start = matcher.start()
54- val end = matcher.end()
74+ var i = 0
75+ val len = text.length
5576
56- if (start > lastHeight) {
57- val part = text.substring(lastHeight, start)
58- appendStyledText(builder, part)
77+ fun flushPlainBuffer () {
78+ if (plainBuffer.isNotEmpty()) {
79+ appendStyledText(builder, plainBuffer.toString())
80+ plainBuffer.setLength(0 )
5981 }
82+ }
83+
84+ while (i < len) {
85+ val c = text[i]
6086
61- val paramsStr = matcher.group(1 ) ? : " "
62- parseAnsiParameters(paramsStr)
63- lastHeight = end
87+ if (c != ' \u001B ' ) {
88+ plainBuffer.append(c)
89+ i++
90+ continue
91+ }
92+
93+ // Gặp ESC -> flush phần text thường đã tích lũy trước khi xử lý escape
94+ flushPlainBuffer()
95+
96+ val nextChar = if (i + 1 < len) text[i + 1 ] else ' \u0000 '
97+
98+ when (nextChar) {
99+ ' [' -> {
100+ val m = CSI_PATTERN .matcher(text)
101+ m.region(i, len)
102+ if (m.lookingAt()) {
103+ val params = m.group(1 ) ? : " "
104+ val finalByte = m.group(3 )
105+ if (finalByte == " m" ) {
106+ parseAnsiParameters(params)
107+ }
108+ // Các CSI khác (cursor move, clear line, hide cursor...) -> bỏ qua, không render
109+ i = m.end()
110+ } else {
111+ i++ // escape hỏng/không đầy đủ, bỏ qua 1 ký tự để tránh vòng lặp vô hạn
112+ }
113+ }
114+
115+ ' ]' -> {
116+ val m = OSC_PATTERN .matcher(text)
117+ m.region(i, len)
118+ if (m.lookingAt()) {
119+ val body = m.group(1 ) ? : " "
120+ if (body.startsWith(" 8;" )) {
121+ // OSC 8 hyperlink: "8;params;URL"
122+ val url = body.substringAfterLast(' ;' , " " )
123+ if (url.isEmpty()) {
124+ // Thẻ đóng link
125+ if (pendingLinkUrl != null && pendingLinkStart in 0 .. builder.length) {
126+ builder.setSpan(
127+ URLSpan (pendingLinkUrl),
128+ pendingLinkStart,
129+ builder.length,
130+ Spannable .SPAN_EXCLUSIVE_EXCLUSIVE
131+ )
132+ }
133+ pendingLinkUrl = null
134+ pendingLinkStart = - 1
135+ } else {
136+ // Thẻ mở link
137+ pendingLinkUrl = url
138+ pendingLinkStart = builder.length
139+ }
140+ }
141+ // Các OSC khác (đặt title cửa sổ, v.v.) -> bỏ qua, không hiển thị ra text
142+ i = m.end()
143+ } else {
144+ i++
145+ }
146+ }
147+
148+ else -> {
149+ val m = SIMPLE_ESC_PATTERN .matcher(text)
150+ m.region(i, len)
151+ if (m.lookingAt()) {
152+ i = m.end() // bỏ qua, không có nội dung hiển thị
153+ } else {
154+ i++ // ESC đơn lẻ không rõ dạng -> bỏ qua chính nó
155+ }
156+ }
157+ }
64158 }
65159
66- if (lastHeight < text.length) {
67- val part = text.substring(lastHeight)
68- appendStyledText(builder, part)
160+ flushPlainBuffer()
161+
162+ // Nếu chuỗi kết thúc mà link vẫn chưa đóng (log bị cắt giữa chừng), vẫn áp span cho phần đã có
163+ if (pendingLinkUrl != null && pendingLinkStart in 0 until builder.length) {
164+ builder.setSpan(
165+ URLSpan (pendingLinkUrl),
166+ pendingLinkStart,
167+ builder.length,
168+ Spannable .SPAN_EXCLUSIVE_EXCLUSIVE
169+ )
69170 }
70171
71172 return builder
@@ -81,47 +182,78 @@ object AnsiColorParser {
81182
82183 if (spanStart == spanEnd) return
83184
84- // 1. Áp dụng màu chữ (Foreground)
185+ var fg = currentFgColor
186+ var bg = currentBgColor
187+
188+ // Reverse video: hoán đổi fg/bg khi hiển thị
189+ if (isReverse) {
190+ val realBg = if (bg == Color .TRANSPARENT ) Color .BLACK else bg
191+ val tmp = fg
192+ fg = realBg
193+ bg = tmp
194+ }
195+
196+ // Dim/faint: giảm độ chói của màu chữ
197+ if (isDim) {
198+ fg = Color .argb(
199+ 180 ,
200+ Color .red(fg), Color .green(fg), Color .blue(fg)
201+ )
202+ }
203+
85204 builder.setSpan(
86- ForegroundColorSpan (currentFgColor),
87- spanStart,
88- spanEnd,
205+ ForegroundColorSpan (fg),
206+ spanStart, spanEnd,
89207 Spannable .SPAN_EXCLUSIVE_EXCLUSIVE
90208 )
91209
92- // 2. Áp dụng màu nền (Background) nếu có thiết lập màu khác trong suốt
93- if (currentBgColor != Color .TRANSPARENT ) {
210+ if (bg != Color .TRANSPARENT ) {
94211 builder.setSpan(
95- BackgroundColorSpan (currentBgColor),
96- spanStart,
97- spanEnd,
212+ BackgroundColorSpan (bg),
213+ spanStart, spanEnd,
98214 Spannable .SPAN_EXCLUSIVE_EXCLUSIVE
99215 )
100216 }
101217
102- // 3. Áp dụng kiểu chữ in đậm (Bold)
103- if (isBold) {
218+ if (isBold && isItalic) {
219+ builder.setSpan(
220+ StyleSpan (Typeface .BOLD_ITALIC ),
221+ spanStart, spanEnd,
222+ Spannable .SPAN_EXCLUSIVE_EXCLUSIVE
223+ )
224+ } else if (isBold) {
104225 builder.setSpan(
105226 StyleSpan (Typeface .BOLD ),
106- spanStart,
107- spanEnd,
227+ spanStart, spanEnd,
228+ Spannable .SPAN_EXCLUSIVE_EXCLUSIVE
229+ )
230+ } else if (isItalic) {
231+ builder.setSpan(
232+ StyleSpan (Typeface .ITALIC ),
233+ spanStart, spanEnd,
108234 Spannable .SPAN_EXCLUSIVE_EXCLUSIVE
109235 )
110236 }
111237
112- // 4. Áp dụng kiểu chữ gạch chân (Underline)
113238 if (isUnderline) {
114239 builder.setSpan(
115240 UnderlineSpan (),
116- spanStart,
117- spanEnd,
241+ spanStart, spanEnd,
242+ Spannable .SPAN_EXCLUSIVE_EXCLUSIVE
243+ )
244+ }
245+
246+ if (isStrikethrough) {
247+ builder.setSpan(
248+ StrikethroughSpan (),
249+ spanStart, spanEnd,
118250 Spannable .SPAN_EXCLUSIVE_EXCLUSIVE
119251 )
120252 }
121253 }
122254
123255 /* *
124- * Parse chuỗi tham số ANSI để cập nhật trạng thái định dạng (in đậm, màu chữ, màu nền,...)
256+ * Parse chuỗi tham số SGR để cập nhật trạng thái định dạng
125257 */
126258 private fun parseAnsiParameters (paramsStr : String ) {
127259 if (paramsStr.isEmpty() || paramsStr == " 0" ) {
@@ -135,31 +267,32 @@ object AnsiColorParser {
135267 while (i < tokens.size) {
136268 val code = tokens[i].toIntOrNull() ? : 0
137269 when {
138- // Reset định dạng về mặc định
139270 code == 0 -> resetToDefault()
140271
141- // Kiểu chữ (Style)
142272 code == 1 -> isBold = true
273+ code == 2 -> isDim = true
274+ code == 3 -> isItalic = true
143275 code == 4 -> isUnderline = true
144- code == 21 -> isBold = false // Tắt in đậm (Double underline hoặc bold off tùy terminal)
145- code == 22 -> isBold = false // Tắt in đậm thông thường
146- code == 24 -> isUnderline = false // Tắt gạch chân
276+ code == 7 -> isReverse = true
277+ code == 9 -> isStrikethrough = true
147278
148- // Màu chữ mặc định (Default Foreground)
149- code == 39 -> currentFgColor = DEFAULT_FOREGROUND
279+ code == 21 -> isBold = false // double underline / bold-off tùy terminal
280+ code == 22 -> { isBold = false ; isDim = false }
281+ code == 23 -> isItalic = false
282+ code == 24 -> isUnderline = false
283+ code == 27 -> isReverse = false
284+ code == 29 -> isStrikethrough = false
150285
151- // Màu nền mặc định (Default Background)
286+ code == 39 -> currentFgColor = DEFAULT_FOREGROUND
152287 code == 49 -> currentBgColor = DEFAULT_BACKGROUND
153288
154- // Màu chữ cơ bản (30 - 37) và màu chữ sáng (90 - 97)
155289 code in 30 .. 37 -> currentFgColor = ANSI_16_COLORS [code - 30 ]
156290 code in 90 .. 97 -> currentFgColor = ANSI_16_COLORS [code - 90 + 8 ]
157291
158- // Màu nền cơ bản (40 - 47) và màu nền sáng (100 - 107)
159292 code in 40 .. 47 -> currentBgColor = ANSI_16_COLORS [code - 40 ]
160293 code in 100 .. 107 -> currentBgColor = ANSI_16_COLORS [code - 100 + 8 ]
161294
162- // Chế độ màu chữ mở rộng ( 38;5;ID hoặc 38;2;R;G;B)
295+ // Màu chữ mở rộng: 38;5;ID hoặc 38;2;R;G;B
163296 code == 38 -> {
164297 if (i + 1 < tokens.size) {
165298 val mode = tokens[i + 1 ].toIntOrNull() ? : 0
@@ -177,7 +310,7 @@ object AnsiColorParser {
177310 }
178311 }
179312
180- // Chế độ màu nền mở rộng ( 48;5;ID hoặc 48;2;R;G;B)
313+ // Màu nền mở rộng: 48;5;ID hoặc 48;2;R;G;B
181314 code == 48 -> {
182315 if (i + 1 < tokens.size) {
183316 val mode = tokens[i + 1 ].toIntOrNull() ? : 0
@@ -221,10 +354,14 @@ object AnsiColorParser {
221354 currentFgColor = DEFAULT_FOREGROUND
222355 currentBgColor = DEFAULT_BACKGROUND
223356 isBold = false
357+ isItalic = false
224358 isUnderline = false
359+ isStrikethrough = false
360+ isReverse = false
361+ isDim = false
225362 }
226363
227364 fun reset () {
228365 resetToDefault()
229366 }
230- }
367+ }
0 commit comments