using fandoc
using gfx::Color
// TODO create a tput command that takes escape aliases - see http://linuxcommand.org/lc3_adv_tput.php
** Generates a sequence of ANSI escape codes.
**
** Note that generated SGR command sequences are optimised where possible.
class AnsiBuf {
** The 'ESC' char, '0x1B'.
static const Int ESC := 0x1B
private StrBuf buf := StrBuf()
private Bool inSgr := false
** Creates an 'AnsiBuf' instance , optionally with the given string / ANSI sequence.
new make(Str? str := null) {
if (str != null)
buf.add(str)
}
** Writes the *Control Sequence Initiator*.
**
** ansi-sequence: ESC[
This csi() {
endSgr.addChar(ESC).addChar('[')
return this
}
// ---- Cursor methods ----------------------------------------------------
** Moves the cursor up a number of rows.
** Passing in '0' or 'null' does nothing.
**
** ansi-sequence: ESC[${rows}A
This curUp(Int? rows := 1) {
if (rows == null || rows == 0) return this
if (rows < 0 || rows > 0xFF)
throw ArgErr("Invalid row: 0..0xFF != $rows")
return csi.add(rows.toStr).addChar('A')
}
** Moves the cursor up a number of rows.
** Passing in '0' or 'null' does nothing.
**
** ansi-sequence: ESC[${rows}B
This curDown(Int? rows := 1) {
if (rows == null || rows == 0) return this
if (rows < 0 || rows > 0xFF)
throw ArgErr("Invalid row: 0..0xFF != $rows")
return csi.add(rows.toStr).addChar('B')
}
** Moves the cursor left a number of rows.
** Passing in '0' or 'null' does nothing.
**
** ansi-sequence: ESC[${cols}D
This curLeft(Int? cols := 1) {
if (cols == null || cols == 0) return this
if (cols < 0 || cols > 0xFF)
throw ArgErr("Invalid column: 0..0xFF != $cols")
return csi.add(cols.toStr).addChar('D')
}
** Moves the cursor right a number of rows.
** Passing in '0' or 'null' does nothing.
**
** ansi-sequence: ESC[${cols}C
This curRight(Int? cols := 1) {
if (cols == null || cols == 0) return this
if (cols < 0 || cols > 0xFF)
throw ArgErr("Invalid column: 0..0xFF != $cols")
return csi.add(cols.toStr).addChar('C')
}
** Moves the cursor to an absolute column position.
** Note that ANSI standards state that 'column' is 1 based, so 'curHorizonal(1)' returns the cursor to the start of the line.
** Passing in '0' or 'null' does nothing.
**
** ansi-sequence: ESC[${col}G
This curHorizonal(Int? column := 1) {
if (column == null || column == 0) return this
if (column < 1 || column > 0xFF)
throw ArgErr("Invalid column: 0..0xFF != $column")
return csi.add(column.toStr).addChar('G')
}
** Moves the cursor to the start of the current line.
**
** ansi-sequence: ESC[v
**
** Note this is an Alien-Factory extension - not an ANSI standard.
This curHome() {
// TODO Linux Terminal - this is not an ANSI standard (curHome)
csi.addChar('v')
}
** Moves the cursor to the end of the current line.
**
** ansi-sequence: ESC[w
**
** Note this is an Alien-Factory extension - not an ANSI standard.
This curEnd() {
// TODO Linux Terminal - this is not an ANSI standard (curEnd)
csi.addChar('w')
}
** Saves the cursor position - both horizontal and vertical.
**
** ansi-sequence: ESC[s
This curSave() {
csi.addChar('s')
}
** Restores the cursor position - both horizontal and vertical.
** Does nothing if a cursor position has not yet been saved.
**
** ansi-sequence: ESC[u
This curRestore() {
csi.addChar('u')
}
// ---- Clear methods -----------------------------------------------------
** Clears the screen.
**
** ansi-sequence: ESC[2J
This clearScreen() {
csi.addChar('2').addChar('J')
}
** Clears the current line.
** The cursor position is unaffected.
**
** ansi-sequence: ESC[2K
This clearLine() {
csi.addChar('2').addChar('K')
}
** Clears the current line from the cursor to the start.
** The cursor position is unaffected.
**
** ansi-sequence: ESC[1K
This clearLineToStart() {
csi.addChar('1').addChar('K')
}
** Clears the current line from the cursor to the end.
** The cursor position is unaffected.
**
** ansi-sequence: ESC[0K
This clearLineToEnd() {
csi.addChar('0').addChar('K')
}
// ---- Colour methods ----------------------------------------------------
** Resets text to:
** - default foreground colour
** - default background colour
** - non-bold
** - non-italics
** - no underline
**
** ansi-sequence: ESC[m
This reset() {
endSgr.addChar(ESC).addChar('[').addChar('m')
return this
}
** Sets the foreground colour to the given RGB integer.
** If 'null' is passed, this method does nothing.
**
** - bits 16-23 red
** - bits 8-15 green
** - bits 0-7 blue
**
** For example orange would be '0xFF_A5_00'.
**
** ansi-sequence: ESC[38;2;${r};${g};${b}m
This fgRgb(Int? rgb) {
if (rgb == null) return this
if (rgb < 0 || rgb > 0xFF_FF_FF)
throw ArgErr("Invalid colour index: 0..0xFFFFFF != $rgb")
return fg(Color(rgb))
}
** Sets the foreground colour to the given Color. Any alpha value is ignored.
** If 'null' is passed, this method does nothing.
**
** ansi-sequence: ESC[38;2;${r};${g};${b}m
This fg(Color? col) {
if (col == null) return this
startSgr.addChar('3').addChar('8').addChar(';').addChar('2').addChar(';').add(col.r.toStr).addChar(';').add(col.g.toStr).addChar(';').add(col.b.toStr)
return this
}
** Sets the foreground colour to the given palette colour. (0..255)
** If 'null' is passed, this method does nothing.
**
** ansi-sequence: ESC[38;5;${i}m
This fgIdx(Int? i) {
if (i == null) return this
if (i < 0 || i > 255)
throw ArgErr("Invalid colour index: 0..255 != $i")
startSgr.addChar('3').addChar('8').addChar(';').addChar('5').addChar(';').add(i.toStr)
return this
}
** Resets the foreground colour to default.
**
** ansi-sequence: ESC[39m
This fgReset() {
startSgr.addChar('3').addChar('9')
return this
}
** Sets the background colour to the given RGB integer.
** If 'null' is passed, this method does nothing.
**
** - bits 16-23 red
** - bits 8-15 green
** - bits 0-7 blue
**
** For example orange would be '0xFF_A5_00'.
**
** ansi-sequence: ESC[48;2;${r};${g};${b}m
This bgRgb(Int? rgb) {
if (rgb == null) return this
if (rgb < 0 || rgb > 0xFF_FF_FF)
throw ArgErr("Invalid colour index: 0..0xFFFFFF != $rgb")
return bg(Color(rgb))
}
** Sets the background colour to the given Color. Any alpha value is ignored.
** If 'null' is passed, this method does nothing.
**
** ansi-sequence: ESC[48;2;${r};${g};${b}m
This bg(Color? col) {
if (col == null) return this
startSgr.addChar('4').addChar('8').addChar(';').addChar('2').addChar(';').add(col.r.toStr).addChar(';').add(col.g.toStr).addChar(';').add(col.b.toStr)
return this
}
** Sets the background colour to the given palette colour (0..255).
** If 'null' is passed, this method does nothing.
**
** ansi-sequence: ESC[48;5;${i}m
This bgIdx(Int? i) {
if (i == null) return this
if (i < 0 || i > 255)
throw ArgErr("Invalid colour index: 0..255 != $i")
startSgr.addChar('4').addChar('8').addChar(';').addChar('5').addChar(';').add(i.toStr)
return this
}
** Resets the foreground colour to default.
**
** ansi-sequence: ESC[49m
This bgReset() {
startSgr.addChar('4').addChar('9')
return this
}
** Turns bold on or off.
**
** ansi-sequence: ESC[1m or ESC[21m
This bold(Bool onOff := true) {
if (onOff)
startSgr.addChar('1')
else
startSgr.addChar('2').addChar('1')
return this
}
** Turns italics on or off.
**
** ansi-sequence: ESC[3m or ESC[23m
This italic(Bool onOff := true) {
if (onOff)
startSgr.addChar('3')
else
startSgr.addChar('2').addChar('3')
return this
}
** Turns underline on or off.
**
** ansi-sequence: ESC[4m or ESC[24m
This underline(Bool onOff := true) {
if (onOff)
startSgr.addChar('4')
else
startSgr.addChar('2').addChar('4')
return this
}
** Turns crossed out on or off.
**
** ansi-sequence: ESC[4m or ESC[24m
**
** Note this is represented by a squiggly underline in the ANSI Terminal.
This crossedOut(Bool onOff := true) {
if (onOff)
startSgr.addChar('9')
else
startSgr.addChar('2').addChar('9')
return this
}
** Turns concealed text on or off.
**
** ansi-sequence: ESC[8m or ESC[28m
**
** *Not implemented.*
This conceal(Bool onOff := true) {
if (onOff)
startSgr.addChar('8')
else
startSgr.addChar('2').addChar('8')
return this
}
** Optimised implementation for 'print(ch.toChar)'.
This printChar(Int ch) {
endSgr.addChar(ch)
return this
}
** Adds 'x.toStr' to the end of this buffer. If 'x' is null then the string "null" is added.
This print(Obj? x := "") {
endSgr.add(x)
return this
}
** Adds 'x.toStr + "\n"' to the end of this buffer. If 'x' is null then the string "null" is added.
This printLine(Obj? x := "") {
endSgr.add(x).addChar('\n')
return this
}
** Prints the given fandoc string to the buffer, converting bold and italic formatting to their
** ANSI representation.
**
** If 'maxWidth' is given then all text is wrapped at that width.
**
** TIP: Pass the terminal column width as 'maxWidth' to ensure all text is visible on screen.
This printFandoc(Str fandoc, Int? maxWidth := null) {
endSgr
FandocParser().parseStr(fandoc).write(AnsiDocWriter(this, maxWidth))
return this
}
** Convenience for printChar('\n').
This newLine() {
endSgr.addChar('\n')
return this
}
** Convenience for printChar('\b').
This backspace() {
endSgr.addChar('\b')
return this
}
** Returns the contents of this ANSI buffer as a string.
Str toAnsi() {
endSgr.toStr
}
** Clear the contents of the string buffer so that is has a size of zero. Return this.
This clear() {
buf.clear
inSgr = false
return this
}
** Returns the number of chars in the buf.
Int size() {
buf.size
}
// ---- Convenience methods -----------------------------------------------
** Convenience for 'printChar()'.
This addChar(Int ch) {
printChar(ch)
}
** Convenience for 'printChar()'.
This writeChar(Int ch) {
printChar(ch)
}
** Convenience for 'print()'.
This write(Obj? x) {
print(x)
}
** Convenience for 'print()'.
This add(Obj? x) {
print(x)
}
// ---- Private Stuff ----------------------------------------------------
private StrBuf startSgr() {
if (inSgr)
buf.addChar(';')
else
csi
inSgr = true
return buf
}
private StrBuf endSgr() {
if (inSgr)
buf.addChar('m')
inSgr = false
return buf
}
** Returns 'toAnsi()'.
override Str toStr() {
toAnsi
}
** Returns a copy of this ANSI string with all the escape codes and non-printable characters removed.
Str toPlain() {
removeEscapeCodes(endSgr.toStr)
}
** Removes all escape codes and non-printable characters from the given string.
static Str removeEscapeCodes(Str str) {
expect := expectNothing
newStr := StrBuf()
printChar := |Int char| {
// continue an escape sequence
if (expect > expectNothing) {
if (expect == expectCsi) {
if (char != '[') {
// nothing we can do, except keep calm and carry on
expect = expectNothing
return
}
expect = expectCsiCmd
return
}
if (expect == expectCsiCmd) {
if (char.isDigit) {
return
}
if (char == ';') {
return
}
if (char == 'm') {
expect = expectNothing
return
}
// nothing we can do, except keep calm and carry on
expect = expectNothing
return
}
}
// the start of an escape sequence
if (char == '\u001b') {
expect = expectCsi
return
}
newStr.addChar(char)
}
str.each { printChar(it) }
return newStr.toStr
}
private static const Int expectNothing := 0
private static const Int expectCsi := 1
private static const Int expectCsiCmd := 2
}