Files
luma/Sources/LumaCore/StyledTextAnsi.swift
Ole André Vadla Ravnås 17500a123a Improve the address insight view
Make hex address operands in the disassembly clickable for the
pointer actions while keeping the text selectable. Overlay the
reread/reanalyze buttons instead of letting them push the content
down.
2026-06-27 19:00:36 +02:00

182 lines
5.5 KiB
Swift

import Foundation
extension StyledText {
public static func parseAnsi(_ input: String) -> StyledText {
var spans: [Span] = []
var current = ""
var fg: RGBColor?
var bg: RGBColor?
var bold = false
var reverseVideo = false
func flush() {
guard !current.isEmpty else { return }
let f = reverseVideo ? bg : fg
let b = reverseVideo ? fg : bg
spans.append(Span(text: current, foreground: f, background: b, isBold: bold))
current = ""
}
var state: State = .printable
var paramBuffer = ""
var params: [String] = []
for char in input {
switch state {
case .printable:
if char == "\u{001B}" {
flush()
state = .escape
} else {
current.append(char)
}
case .escape:
if char == "[" {
params.removeAll(keepingCapacity: true)
paramBuffer.removeAll(keepingCapacity: true)
state = .code
} else {
state = .printable
}
case .code:
if char.isNumber {
paramBuffer.append(char)
} else if char == ";" {
params.append(paramBuffer)
paramBuffer.removeAll(keepingCapacity: true)
} else if char == "m" {
params.append(paramBuffer)
paramBuffer.removeAll(keepingCapacity: true)
applySGR(params, fg: &fg, bg: &bg, bold: &bold, reverseVideo: &reverseVideo)
state = .printable
} else {
state = .printable
}
}
}
flush()
return StyledText(spans: spans)
}
public func addressSegments(minimumValue: UInt64 = 0x1000) -> [AddressSegment] {
let chars = Array(plainText)
var segments: [AddressSegment] = []
var runStart = 0
var i = 0
func flushPlain(upTo end: Int) {
if end > runStart {
segments.append(AddressSegment(text: slice(charRange: runStart..<end), address: nil))
}
}
while i < chars.count {
guard chars[i] == "0", i + 1 < chars.count, chars[i + 1] == "x" || chars[i + 1] == "X" else {
i += 1
continue
}
var j = i + 2
while j < chars.count, chars[j].isHexDigit { j += 1 }
let precededByWord = i > 0 && (chars[i - 1].isLetter || chars[i - 1].isNumber || chars[i - 1] == "_")
if j > i + 2, !precededByWord,
let value = UInt64(String(chars[(i + 2)..<j]), radix: 16), value >= minimumValue
{
flushPlain(upTo: i)
segments.append(AddressSegment(text: slice(charRange: i..<j), address: value))
runStart = j
}
i = j
}
flushPlain(upTo: chars.count)
return segments
}
public func slice(charRange: Range<Int>) -> StyledText {
var out: [Span] = []
var cursor = 0
let lower = charRange.lowerBound
let upper = charRange.upperBound
for span in spans {
let spanLen = span.text.count
let spanEnd = cursor + spanLen
if spanEnd <= lower || cursor >= upper {
cursor = spanEnd
continue
}
let localStart = max(0, lower - cursor)
let localEnd = min(spanLen, upper - cursor)
if localStart < localEnd {
let s = span.text.index(span.text.startIndex, offsetBy: localStart)
let e = span.text.index(span.text.startIndex, offsetBy: localEnd)
out.append(Span(
text: String(span.text[s..<e]),
foreground: span.foreground,
background: span.background,
isBold: span.isBold
))
}
cursor = spanEnd
if cursor >= upper { break }
}
return StyledText(spans: out)
}
}
private enum State {
case printable
case escape
case code
}
private func applySGR(
_ params: [String],
fg: inout RGBColor?,
bg: inout RGBColor?,
bold: inout Bool,
reverseVideo: inout Bool
) {
let normalized: [String] = params.isEmpty ? ["0"] : params.map { $0.isEmpty ? "0" : $0 }
var i = 0
while i < normalized.count {
let p = normalized[i]
switch p {
case "0":
fg = nil
bg = nil
bold = false
reverseVideo = false
case "1":
bold = true
case "22":
bold = false
case "7":
reverseVideo = true
case "27":
reverseVideo = false
case "38", "48":
let isForeground = (p == "38")
guard i + 4 < normalized.count, normalized[i + 1] == "2" else { break }
let r = UInt8(normalized[i + 2]) ?? 0
let g = UInt8(normalized[i + 3]) ?? 0
let b = UInt8(normalized[i + 4]) ?? 0
let color = RGBColor(r: r, g: g, b: b)
if isForeground { fg = color } else { bg = color }
i += 4
case "39":
fg = nil
case "49":
bg = nil
default:
break
}
i += 1
}
}