Skip to content

Instantly share code, notes, and snippets.

@krzyzanowskim
Last active March 27, 2019 10:20
Show Gist options
  • Save krzyzanowskim/37c5da7d74c6b3262f1452f95532fec1 to your computer and use it in GitHub Desktop.
Save krzyzanowskim/37c5da7d74c6b3262f1452f95532fec1 to your computer and use it in GitHub Desktop.
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: &currentIndex)
}
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: &currentIndex), let color = Color(rawValue:value) {
command = .foregroundColor(color)
} else if case .backgroundColor(_) = command, let value = processByteValue(in: string, terminator: Character("m"), at: &currentIndex), let color = Color(rawValue:value) {
command = .backgroundColor(color)
} else if case .paletteForegroundColor(_) = command, let value = processByteValue(in: string, terminator: Character("m"), at: &currentIndex) {
command = .paletteForegroundColor(value)
} else if case .paletteBackgroundColor(_) = command, let value = processByteValue(in: string, terminator: Character("m"), at: &currentIndex) {
command = .paletteBackgroundColor(value)
} else if case .rgbForegroundColor(_,_,_) = command, let value = processRGBValues(in: string, terminator: Character("m"), at: &currentIndex) {
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: &currentIndex) {
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: &currentIndex)
}
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: &currentIndex)
}
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