Last active
March 27, 2019 10:20
-
-
Save krzyzanowskim/37c5da7d74c6b3262f1452f95532fec1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
// Process stream | |
// http://ascii-table.com/ansi-escape-sequences-vt-100.php | |
// http://ascii-table.com/ansi-escape-sequences.php | |
public final class ANSIEscapeProcessor { | |
public func tokens(string: String) -> [Token] { | |
var tokens: [Token] = [] | |
var commandSequence = "" | |
var tokenString = "" | |
var isProcessingCommand = false | |
var currentIndex = string.startIndex | |
while currentIndex != string.endIndex { | |
defer { | |
string.formIndex(after: ¤tIndex) | |
} | |
let character = string[currentIndex] | |
if character.isEsc { | |
commandSequence = "" | |
isProcessingCommand = true | |
if !tokenString.isEmpty { | |
tokens.append(Token(kind: .text(tokenString))) | |
tokenString = "" | |
} | |
tokenString.append(character) | |
continue | |
} | |
if isProcessingCommand { | |
commandSequence.append(character) | |
// recognize sequence | |
let candidates = Command.allCases.filter({ | |
$0.sequence == commandSequence || ($0.sequence.starts(with: "[38;2;") && commandSequence == "[38;2;") | |
}) | |
print(commandSequence) | |
if candidates.count == 1, var command = candidates.first { | |
var sequencePrefix = command.sequence | |
if case .foregroundColor(_) = command, let value = processByteValue(in: string, terminator: Character("m"), at: ¤tIndex), let color = Color(rawValue:value) { | |
command = .foregroundColor(color) | |
} else if case .backgroundColor(_) = command, let value = processByteValue(in: string, terminator: Character("m"), at: ¤tIndex), let color = Color(rawValue:value) { | |
command = .backgroundColor(color) | |
} else if case .paletteForegroundColor(_) = command, let value = processByteValue(in: string, terminator: Character("m"), at: ¤tIndex) { | |
command = .paletteForegroundColor(value) | |
} else if case .paletteBackgroundColor(_) = command, let value = processByteValue(in: string, terminator: Character("m"), at: ¤tIndex) { | |
command = .paletteBackgroundColor(value) | |
} else if case .rgbForegroundColor(_,_,_) = command, let value = processRGBValues(in: string, terminator: Character("m"), at: ¤tIndex) { | |
command = .rgbForegroundColor(value.r, value.g, value.b) | |
sequencePrefix = "[38;2;" | |
} else if case .rgbBackgroundColor(_,_,_) = command, let value = processRGBValues(in: string, terminator: Character("m"), at: ¤tIndex) { | |
command = .rgbBackgroundColor(value.r, value.g, value.b) | |
sequencePrefix = "[48;2;" | |
} | |
tokens.append(Token(kind: .command(command))) | |
tokenString.removeFirst(sequencePrefix.count) // remove command | |
commandSequence = "" | |
isProcessingCommand = false | |
} else if commandSequence.count > 10 { | |
// give up | |
commandSequence = "" | |
isProcessingCommand = false | |
} else { | |
tokenString.append(character) | |
} | |
} else { | |
tokenString.append(character) | |
} | |
} | |
if !tokenString.isEmpty { | |
tokens.append(Token(kind: .text(tokenString))) | |
} | |
return tokens | |
} | |
private func processRGBValues(in string: String, terminator: Character, at index: inout String.Index) -> (r: UInt8, g: UInt8, b: UInt8)? { | |
var currentIndex = index | |
var valuesString = "" | |
while currentIndex < string.endIndex { | |
defer { | |
string.formIndex(after: ¤tIndex) | |
} | |
let character = string[currentIndex] | |
if character.isASCII && character.asciiValue == terminator.asciiValue { | |
let values = valuesString.split(separator: Character(";")).compactMap({ UInt8($0) }) | |
if values.count == 3 { | |
index = currentIndex | |
return (r: values[0], g: values[1], b: values[2]) | |
} | |
} | |
if character == Character(";") || character.isWholeNumber { | |
valuesString.append(character) | |
} | |
} | |
return nil | |
} | |
private func processByteValue(in string: String, terminator: Character, at index: inout String.Index) -> UInt8? { | |
var currentIndex = index | |
var valueString = "" | |
while currentIndex < string.endIndex { | |
defer { | |
string.formIndex(after: ¤tIndex) | |
} | |
let character = string[currentIndex] | |
if character.isASCII && character.asciiValue == terminator.asciiValue { | |
index = currentIndex | |
return UInt8(valueString) | |
} | |
if character.isWholeNumber { | |
valueString.append(character) | |
} | |
} | |
return nil | |
} | |
} | |
public extension ANSIEscapeProcessor { | |
enum Color: UInt8, CaseIterable { | |
case black = 0 | |
case red = 1 | |
case green = 2 | |
case yellow = 3 | |
case nlue = 4 | |
case magenta = 5 | |
case cyan = 6 | |
case white = 7 | |
} | |
} | |
public extension ANSIEscapeProcessor { | |
enum Command: CaseIterable { | |
case setNewLineMode | |
case setCursorKeyToApplication | |
case setNumberOfColumnsTo132 | |
case setSmoothScrolling | |
case setReverseVideoOnScreen | |
case setOriginToRelative | |
case setAutoWrapMode | |
case setAutoRepeatMode | |
case setInterlacingMode | |
case setLineFeedMode | |
case setCursorKeyToCursor | |
case setVT52VersusANSI | |
case setNumberOfColumnsTo80 | |
case setJumpScrolling | |
case setNormalVideoOnScreen | |
case setOriginToAbsolute | |
case resetAutoWrapMode | |
case resetAutoRepeatMode | |
case resetInterlacingMode | |
case setAlternateKeypadMode | |
case setNumericKeypadMode | |
case turnOffCharacterAttributes | |
case turnOffCharacterAttributes0 | |
case turnBoldModeOn | |
case turnLowIntensityModeOn | |
case turnUnderlineModeOn | |
case turnBlinkingModeOn | |
case turnRapidBlinkingModeOn | |
case turnReverseVideoOn | |
case turnInvisibleTextModeOn | |
case foregroundColor(Color) | |
case backgroundColor(Color) | |
case paletteForegroundColor(UInt8) | |
case paletteBackgroundColor(UInt8) | |
case rgbForegroundColor(_ r: UInt8, _ g: UInt8, _ b: UInt8) | |
case rgbBackgroundColor(_ r: UInt8, _ g: UInt8, _ b: UInt8) | |
case moveScrollWindowUpOneLine | |
case moveScrollWindowDownOneLine | |
case moveToNextLine | |
case saveCursorPositionAndAttributes | |
case restoreCursorPositionAndAttributes | |
case setATabAtTheCurrentColumn | |
case clearATabAtTheCurrentColumn | |
case clearATabAtTheCurrentColumn0 | |
case clearAllTabs | |
case doubleHeightLettersTopHalf | |
case doubleHeightLettersBottomHalf | |
case singleWidthSingleHeightLetters | |
case doubleWidthSingleHeightLetters | |
case clearLineFromCursorRight | |
case clearLineFromCursorRight0 | |
case clearLineFromCursorLeft | |
case clearEntireLine | |
case clearScreenFromCursorDown | |
case clearScreenFromCursorDown0 | |
case clearScreenFromCursorUp | |
case clearEntireScreen | |
case resetTerminalToInitialState | |
var sequence: String { | |
switch self { | |
case .setNewLineMode: return "[20h" | |
case .setCursorKeyToApplication: return "[?1h" | |
case .setNumberOfColumnsTo132: return "[?3h" | |
case .setSmoothScrolling: return "[?4h" | |
case .setReverseVideoOnScreen: return "[?5h" | |
case .setOriginToRelative: return "[?6h" | |
case .setAutoWrapMode: return "[?7h" | |
case .setAutoRepeatMode: return "[?8h" | |
case .setInterlacingMode: return "[?9h" | |
case .setLineFeedMode: return "[20l" | |
case .setCursorKeyToCursor: return "[?1l" | |
case .setVT52VersusANSI: return "[?2l" | |
case .setNumberOfColumnsTo80: return "[?3l" | |
case .setJumpScrolling: return "[?4l" | |
case .setNormalVideoOnScreen: return "[?5l" | |
case .setOriginToAbsolute: return "[?6l" | |
case .resetAutoWrapMode: return "[?7l" | |
case .resetAutoRepeatMode: return "[?8l" | |
case .resetInterlacingMode: return "[?9l" | |
case .setAlternateKeypadMode: return "=" | |
case .setNumericKeypadMode: return ">" | |
case .turnOffCharacterAttributes: return "[m" | |
case .turnOffCharacterAttributes0: return "[0m" | |
case .turnBoldModeOn: return "[1m" | |
case .turnLowIntensityModeOn: return "[2m" | |
case .turnUnderlineModeOn: return "[4m" | |
case .turnBlinkingModeOn: return "[5m" | |
case .turnRapidBlinkingModeOn: return "[6m" | |
case .turnReverseVideoOn: return "[7m" | |
case .turnInvisibleTextModeOn: return "[8m" | |
case .foregroundColor(let color): return "[3\(color.rawValue)m" | |
case .backgroundColor(let color): return "[4\(color.rawValue)m" | |
case .paletteForegroundColor(let color): return "[38;5;\(color)m" | |
case .paletteBackgroundColor(let color): return "[48;5;\(color)m" | |
case .rgbForegroundColor(let r, let g, let b): return "[38;2;\(r);\(g);\(b)m" | |
case .rgbBackgroundColor(let r, let g, let b): return "[48;2;\(r);\(g);\(b)m" | |
case .moveScrollWindowUpOneLine: return "D" | |
case .moveScrollWindowDownOneLine: return "M" | |
case .moveToNextLine: return "E" | |
case .saveCursorPositionAndAttributes: return "7" | |
case .restoreCursorPositionAndAttributes: return "8" | |
case .setATabAtTheCurrentColumn: return "H" | |
case .clearATabAtTheCurrentColumn: return "[g" | |
case .clearATabAtTheCurrentColumn0: return "[0g" | |
case .clearAllTabs: return "[3g" | |
case .doubleHeightLettersTopHalf: return "#3" | |
case .doubleHeightLettersBottomHalf: return "#4" | |
case .singleWidthSingleHeightLetters: return "#5" | |
case .doubleWidthSingleHeightLetters: return "#6" | |
case .clearLineFromCursorRight: return "[K" | |
case .clearLineFromCursorRight0: return "[0K" | |
case .clearLineFromCursorLeft: return "[1K" | |
case .clearEntireLine: return "[2K" | |
case .clearScreenFromCursorDown: return "[J" | |
case .clearScreenFromCursorDown0: return "[0J" | |
case .clearScreenFromCursorUp: return "[1J" | |
case .clearEntireScreen: return "[2J" | |
case .resetTerminalToInitialState: return "c" | |
} | |
} | |
// rgb color is fake, and is resolved while parsing | |
public static var allCases: [Command] { | |
return Color.allCases.map({.foregroundColor($0)}) + Color.allCases.map({.backgroundColor($0)}) as [Command] | |
+ (UInt8.min...UInt8.max).map({.paletteForegroundColor($0)}) + (UInt8.min...UInt8.max).map({.paletteBackgroundColor($0)}) as [Command] | |
+ [.rgbForegroundColor(0,0,0), .rgbBackgroundColor(0,0,0)] as [Command] | |
+ [.setNewLineMode, .setCursorKeyToApplication, .setNumberOfColumnsTo132, | |
.setSmoothScrolling, .setReverseVideoOnScreen, .setOriginToRelative, | |
.setAutoWrapMode, .setAutoRepeatMode, .setInterlacingMode, | |
.setLineFeedMode, .setCursorKeyToCursor, .setVT52VersusANSI, | |
.setNumberOfColumnsTo80, .setJumpScrolling, .setNormalVideoOnScreen, | |
.setOriginToAbsolute, .resetAutoWrapMode, .resetAutoRepeatMode, | |
.resetInterlacingMode, .setAlternateKeypadMode, .setNumericKeypadMode, | |
.turnOffCharacterAttributes, .turnOffCharacterAttributes0, .turnBoldModeOn, | |
.turnLowIntensityModeOn, .turnUnderlineModeOn, .turnBlinkingModeOn, | |
.turnRapidBlinkingModeOn, .turnReverseVideoOn, .turnInvisibleTextModeOn, | |
.moveScrollWindowUpOneLine, .moveScrollWindowDownOneLine, .moveToNextLine, | |
.saveCursorPositionAndAttributes, .restoreCursorPositionAndAttributes, .setATabAtTheCurrentColumn, | |
.clearATabAtTheCurrentColumn, .clearATabAtTheCurrentColumn0, .clearAllTabs, | |
.doubleHeightLettersTopHalf, .doubleHeightLettersBottomHalf, .singleWidthSingleHeightLetters, | |
.doubleWidthSingleHeightLetters, .clearLineFromCursorRight, .clearLineFromCursorRight0, | |
.clearLineFromCursorLeft, .clearEntireLine, .clearScreenFromCursorDown, | |
.clearScreenFromCursorDown0, .clearScreenFromCursorUp, .clearEntireScreen, .resetTerminalToInitialState] | |
} | |
} | |
} | |
public extension ANSIEscapeProcessor { | |
enum Kind { | |
case text(String) | |
case command(Command) | |
} | |
} | |
public extension ANSIEscapeProcessor { | |
struct Token { | |
let kind: Kind | |
} | |
} | |
private extension Character { | |
var isEsc: Bool { | |
return self.isASCII && self.asciiValue == 0x1B | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment