All Downloads are FREE. Search and download functionalities are using the official Maven repository.

caseapp.core.util.Fansi.scala Maven / Gradle / Ivy

There is a newer version: 2.1.0-M29
Show newest version
package caseapp.core.util
package fansi

import java.util

import scala.language.implicitConversions

import scala.annotation.tailrec
import scala.collection.mutable

/** Encapsulates a string with associated ANSI colors and text decorations.
  *
  * This is your primary data-type when you are dealing with colored fansi strings.
  *
  * Contains some basic string methods, as well as some ansi methods to e.g. apply particular colors
  * or other decorations to particular sections of the [[fansi.Str]]. [[render]] flattens it out
  * into a `java.lang.String` with all the colors present as ANSI escapes.
  *
  * Avoids using Scala collections operations in favor of util.Arrays, giving 20% (on `++`) to
  * >1000% (on `splitAt`, `subString` and `Str.parse`) speedups
  */
case class Str private (private val chars: Array[Char], private val colors: Array[Str.State]) {
  require(chars.length == colors.length)
  override def hashCode() = util.Arrays.hashCode(chars) + util.Arrays.hashCode(colors)
  override def equals(other: Any) = other match {
    case o: fansi.Str =>
      util.Arrays.equals(chars, o.chars) && util.Arrays.equals(colors, o.colors)
    case _ => false
  }

  /** Concatenates two [[fansi.Str]]s, preserving the colors in each one and avoiding any
    * interference between them
    */
  def ++(other: Str) = {
    val chars2  = new Array[Char](length + other.length)
    val colors2 = new Array[Str.State](length + other.length)
    System.arraycopy(chars, 0, chars2, 0, length)
    System.arraycopy(other.chars, 0, chars2, length, other.length)
    System.arraycopy(colors, 0, colors2, 0, length)
    System.arraycopy(other.colors, 0, colors2, length, other.length)

    Str(chars2, colors2)
  }

  /** Splits an [[fansi.Str]] into two sub-strings, preserving the colors in each one.
    *
    * @param index
    *   the plain-text index of the point within the [[fansi.Str]] you want to use to split it.
    */
  def splitAt(index: Int) = (
    new Str(
      util.Arrays.copyOfRange(chars, 0, index),
      util.Arrays.copyOfRange(colors, 0, index)
    ),
    new Str(
      util.Arrays.copyOfRange(chars, index, length),
      util.Arrays.copyOfRange(colors, index, length)
    )
  )

  /** Returns an [[fansi.Str]] which is a substring of this string, and has the same colors as the
    * original section of this string did
    */
  def substring(start: Int = 0, end: Int = length) = {
    require(
      start >= 0 && start <= length,
      s"substring start parameter [$start] must be between 0 and length:$length"
    )
    require(
      end >= start && end <= length,
      s"substring end parameter [$end] must be between start $start and length:$length"
    )
    new Str(
      util.Arrays.copyOfRange(chars, start, end),
      util.Arrays.copyOfRange(colors, start, end)
    )
  }

  /** The plain-text length of this [[fansi.Str]], in UTF-16 characters (same as `.length` on a
    * `java.lang.String`). If you want fancy UTF-8 lengths, use `.plainText`
    */
  def length = chars.length

  override def toString = render

  /** The plain-text `java.lang.String` represented by this [[fansi.Str]], without all the fansi
    * colors or other decorations
    */
  lazy val plainText = new String(chars)

  /** Returns a copy of the colors array backing this `fansi.Str`, in case you want to use it to
    */
  def getColors = colors.clone()

  /** Retrieve the color of this string at the given character index
    */
  def getColor(i: Int) = colors(i)

  /** Returns a copy of the character array backing this `fansi.Str`, in case you want to use it to
    */
  def getChars = chars.clone()

  /** Retrieve the character of this string at the given character index
    */
  def getChar(i: Int) = chars(i)

  /** Converts this [[fansi.Str]] into a `java.lang.String`, including all the fancy fansi colors or
    * decorations as fansi escapes embedded within the string. "Terminates" colors at the right-most
    * end of the resultant `java.lang.String`, making it safe to concat-with or embed-inside other
    * `java.lang.String` without worrying about fansi colors leaking out of it.
    */
  def render = {
    // Pre-size StringBuilder with approximate size (ansi colors tend
    // to be about 5 chars long) to avoid re-allocations during growth
    val output = new StringBuilder(chars.length + colors.length * 5)

    var currentState: Str.State = 0

    // Make a local array copy of the immutable Vector, for maximum performance
    // since the Vector is small and we'll be looking it up over & over & over
    val categoryArray = Attr.categories.toArray

    var i = 0
    while (i < colors.length) {
      // Emit ANSI escapes to change colors where necessary
      // fast-path optimization to check for integer equality first before
      // going through the whole `enableDiff` rigmarole
      if (colors(i) != currentState) {
        Attrs.emitAnsiCodes0(currentState, colors(i), output, categoryArray)
        currentState = colors(i)
      }
      output.append(chars(i))
      i += 1
    }

    // Cap off the left-hand-side of the rendered string with any ansi escape
    // codes necessary to rest the state to 0
    Attrs.emitAnsiCodes0(currentState, 0, output, categoryArray)

    output.toString
  }

  /** Overlays the desired color over the specified range of the [[fansi.Str]].
    */
  def overlay(attrs: Attrs, start: Int = 0, end: Int = length) =
    overlayAll(Seq((attrs, start, end)))

  /** Batch version of [[overlay]], letting you apply a bunch of [[Attrs]] onto various parts of the
    * same string in one operation, avoiding the unnecessary copying that would happen if you
    * applied them with [[overlay]] one by one.
    *
    * The input sequence of overlay-tuples is applied from left to right
    */
  def overlayAll(overlays: Seq[(Attrs, Int, Int)]) = {
    val colorsOut = colors.clone()
    for ((attrs, start, end) <- overlays) {
      require(
        end >= start,
        s"end:$end must be greater than start:$end in fansiStr#overlay call"
      )
      require(start >= 0, s"start:$start must be greater than or equal to 0")
      require(
        end <= colors.length,
        s"end:$end must be less than or equal to length:${colors.length}"
      )

      {
        var i = start
        while (i < end) {
          colorsOut(i) = attrs.transform(colorsOut(i))
          i += 1
        }
      }
    }
    new Str(chars, colorsOut)
  }
}

object Str {

  /** An [[fansi.Str]]'s `color`s array is filled with Long, each representing the ANSI state of one
    * character encoded in its bits. Each [[Attr]] belongs to a [[Category]] that occupies a range
    * of bits within each long:
    *
    * 61... 55 54 53 52 51 .... 31 30 29 28 27 26 25 ..... 6 5 4 3 2 1 0 \|--------|
    * \|-----------------------| |-----------------------| | | |bold \| | | | |reversed \| | |
    * \|underlined \| | |foreground-color \| |background-color \|unused
    *
    * The `0000 0000 0000 0000` long corresponds to plain text with no decoration
    */
  type State = Long

  /** Make the construction of [[fansi.Str]]s from `String`s and other `CharSequence`s automatic
    */
  implicit def implicitApply(raw: CharSequence): fansi.Str = apply(raw)

  /** Regex that can be used to identify Ansi escape patterns in a string.
    *
    * Found from: http://stackoverflow.com/a/33925425/871202
    *
    * Which references:
    *
    * http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf
    *
    * Section 5.4: Control Sequences
    */
  val ansiRegex = "(\u009b|\u001b\\[)[0-?]*[ -\\/]*[@-~]".r.pattern

  /** Creates an [[fansi.Str]] from a non-fansi `java.lang.String` or other `CharSequence`.
    *
    * Note that this method is implicit, meaning you can pass in a `java.lang.String` anywhere an
    * `fansi.Str` is required and it will be automatically parsed and converted for you.
    *
    * @param errorMode
    *   Used to control what kind of behavior you get if the input `CharSequence` contains an Ansi
    *   escape not recognized by Fansi as a valid color.
    */
  def apply(raw: CharSequence, errorMode: ErrorMode = ErrorMode.Throw): fansi.Str = {
    // Pre-allocate some arrays for us to fill up. They will probably be
    // too big if the input has any ansi codes at all but that's ok, we'll
    // trim them later.
    val chars  = new Array[Char](raw.length)
    val colors = new Array[Str.State](raw.length)

    var currentColor = 0L
    var sourceIndex  = 0
    var destIndex    = 0
    val length       = raw.length
    while (sourceIndex < length) {
      val char = raw.charAt(sourceIndex)
      if (char == '\u001b' || char == '\u009b') {
        val escapeStartSourceIndex = sourceIndex
        ParseMap.query(raw, escapeStartSourceIndex) match {
          case None => sourceIndex = errorMode.handle(sourceIndex, raw)
          case Some(tuple) =>
            tuple match {
              case (newIndex, Left(color)) =>
                currentColor = color.transform(currentColor)
                sourceIndex += newIndex
              case (newIndex, Right(category)) =>
                // Gross manual char-by-char parsing of the remainder
                // of the True-color escape, to maximize performance
                sourceIndex += newIndex
                def isDigit(index: Int) =
                  index < raw.length && raw.charAt(index) >= '0' && raw.charAt(index) <= '9'
                def checkChar(index: Int, char: Char) =
                  index < raw.length && raw.charAt(index) == char
                def fail() =
                  sourceIndex = errorMode.handle(escapeStartSourceIndex, raw)
                def getNumber() = {
                  var value = 0
                  var count = 0
                  while (isDigit(sourceIndex) && count < 3) {
                    value = value * 10 + (raw.charAt(sourceIndex) - '0').toInt
                    sourceIndex += 1
                    count += 1
                  }
                  value
                }
                if (!isDigit(sourceIndex)) fail()
                else {
                  val r = getNumber()
                  if (!checkChar(sourceIndex, ';') || !isDigit(sourceIndex + 1)) fail()
                  else {
                    sourceIndex += 1
                    val g = getNumber()
                    if (!checkChar(sourceIndex, ';') || !isDigit(sourceIndex + 1)) fail()
                    else {
                      sourceIndex += 1
                      val b = getNumber()
                      if (!checkChar(sourceIndex, 'm')) fail()
                      else {
                        sourceIndex += 1
                        // Manually perform the `transform` for perf to avoid
                        // calling `True` which instantiates/allocaties an `Attr`
                        if (!(0 <= r && r < 256 && 0 <= g && g < 256 && 0 <= b && b < 256)) fail()
                        else
                          currentColor =
                            (currentColor & ~category.mask) |
                              ((273 + category.trueIndex(r, g, b)) << category.offset)
                      }
                    }
                  }
                }
            }
        }
      }
      else {
        colors(destIndex) = currentColor
        chars(destIndex) = char
        sourceIndex += 1
        destIndex += 1
      }
    }

    Str(
      util.Arrays.copyOfRange(chars, 0, destIndex),
      util.Arrays.copyOfRange(colors, 0, destIndex)
    )
  }

  /** Constructs a [[fansi.Str]] from an array of characters and an array of colors. Performs a
    * defensive copy of the arrays, and validates that they both have the same length
    *
    * Useful together with `getChars` and `getColors` if you want to do manual work on the two
    * mutable arrays before stitching them back together into one immutable [[fansi.Str]]
    */
  def fromArrays(chars: Array[Char], colors: Array[Str.State]) =
    new fansi.Str(chars.clone(), colors.clone())

  def join(args: Str*) = {
    val length = args.iterator.map(_.length).sum
    val chars  = new Array[Char](length)
    val colors = new Array[State](length)
    var j      = 0
    for (arg <- args) {
      var i = 0
      while (i < arg.length) {
        chars(j) = arg.getChar(i)
        colors(j) = arg.getColor(i)
        i += 1
        j += 1
      }
    }
    fromArrays(chars, colors)
  }
  private[this] val ParseMap = {
    val pairs = for {
      cat   <- Attr.categories
      color <- cat.all
      str   <- color.escapeOpt
    } yield (str, Left(color))
    val reset = Seq(
      Console.RESET -> Left(Attr.Reset)
    )
    val trueColors = Seq(
      "\u001b[38;2;" -> Right(Color),
      "\u001b[48;2;" -> Right(Back)
    )
    new Trie(pairs ++ reset ++ trueColors)
  }
}

/** Used to control what kind of behavior you get if the a `CharSequence` you are trying to parse
  * into a [[fansi.Str]] contains an Ansi escape not recognized by Fansi as a valid color.
  */
sealed trait ErrorMode {

  /** Given an unknown Ansi escape was found at `sourceIndex` inside your `raw: CharSequence`, what
    * index should you resume parsing at?
    */
  def handle(sourceIndex: Int, raw: CharSequence): Int
}
object ErrorMode {

  /** Throw an exception and abort the parse
    */
  case object Throw extends ErrorMode {
    def handle(sourceIndex: Int, raw: CharSequence) = {
      val matcher = Str.ansiRegex.matcher(raw)
      val detail =
        if (!matcher.find(sourceIndex)) ""
        else {
          val end = matcher.end()
          " " + raw.subSequence(sourceIndex + 1, end)
        }

      throw new IllegalArgumentException(
        s"Unknown ansi-escape$detail at index $sourceIndex " +
          "inside string cannot be parsed into an fansi.Str"
      )
    }
  }

  /** Skip the `\u001b` that kicks off the unknown Ansi escape but leave subsequent characters in
    * place, so the end-user can see that an Ansi escape was entered e.g. via the [A[B[A[C that
    * appears in the result
    */
  case object Sanitize extends ErrorMode {
    def handle(sourceIndex: Int, raw: CharSequence) =
      sourceIndex + 1
  }

  /** Find the end of the unknown Ansi escape and skip over it's characters entirely, so no trace of
    * them appear in the parsed fansi.Str.
    */
  case object Strip extends ErrorMode {
    def handle(sourceIndex: Int, raw: CharSequence) = {
      val matcher = Str.ansiRegex.matcher(raw)
      matcher.find(sourceIndex)
      matcher.end()
    }
  }
}

/** Represents one or more [[fansi.Attr]]s, that can be passed around as a set or combined with
  * other sets of [[fansi.Attr]]s.
  *
  * Note that a single [[Attr]] is a subclass of [[Attrs]]. If you want to know if this contains
  * multiple [[Attr]]s, you should check for [[Attrs.Multiple]].
  */
sealed trait Attrs {

  /** Apply these [[Attrs]] to the given [[fansi.Str]], making it take effect across the entire
    * length of that string.
    */
  def apply(s: fansi.Str) = s.overlay(this, 0, s.length)

  /** Which bits of the [[Str.State]] integer these [[Attrs]] will override when it is applied
    */
  def resetMask: Long

  /** Which bits of the [[Str.State]] integer these [[Attrs]] will set to `1` when it is applied
    */
  def applyMask: Long

  /** Apply the current [[Attrs]] to the [[Str.State]] integer, modifying it to represent the state
    * after all changes have taken effect
    */
  def transform(state: Str.State) = (state & ~resetMask) | applyMask

  /** Combine this [[fansi.Attrs]] with other [[fansi.Attrs]]s, returning one which when applied is
    * equivalent to applying this one and then the `other` one in series.
    */
  def ++(other: fansi.Attrs): fansi.Attrs

}

object Attrs {

  val Empty = Attrs()

  /** Emit the ansi escapes necessary to transition between two states, if necessary, as a
    * `java.lang.String`
    */
  def emitAnsiCodes(currentState: Str.State, nextState: Str.State) = {
    val output        = new StringBuilder
    val categoryArray = Attr.categories.toArray
    emitAnsiCodes0(currentState, nextState, output, categoryArray)
    output.toString
  }

  /** Messy-but-fast version of [[emitAnsiCodes]] that avoids allocating things unnecessarily. Reads
    * it's category listing from a fast Array version of Attrs.categories and writes it's output to
    * a mutable `StringBuilder`
    */
  def emitAnsiCodes0(
    currentState: Str.State,
    nextState: Str.State,
    output: StringBuilder,
    categoryArray: Array[Category]
  ) =
    if (currentState != nextState) {

      val hardOffMask = Bold.mask
      // Any of these transitions from 1 to 0 within the hardOffMask
      // categories cannot be done with a single ansi escape, and need
      // you to emit a RESET followed by re-building whatever ansi state
      // you previous had from scratch
      val currentState2 =
        if ((currentState & ~nextState & hardOffMask) != 0) {
          output.append(Console.RESET)
          0L
        }
        else
          currentState

      var categoryIndex = 0
      while (categoryIndex < categoryArray.length) {
        val cat = categoryArray(categoryIndex)
        if ((cat.mask & currentState2) != (cat.mask & nextState)) {
          val escape = cat.lookupEscape(nextState & cat.mask)
          output.append(escape)
        }
        categoryIndex += 1
      }
    }

  def apply(attrs: Attr*): Attrs = {
    var output    = List.empty[Attr]
    var resetMask = 0L
    var applyMask = 0L
    // Walk the list of attributes backwards, and aggregate only those whose
    // `resetMask` is not going to get totally covered by the union of all
    // `resetMask`s that come after it.
    //
    // Simultaneously build up the `applyMask`, which is the `applyMask` of
    // all aggregated `attr`s whose own `applyMask` is not totally covered by
    // the union of all `resetMask`s that come after.
    for (attr <- attrs.reverseIterator)
      if ((attr.resetMask & ~resetMask) != 0) {
        if ((attr.applyMask & resetMask) == 0) applyMask = applyMask | attr.applyMask
        resetMask = resetMask | attr.resetMask
        output = attr :: output
      }

    if (output.length == 1) output.head
    else new Multiple(resetMask, applyMask, output.toArray.reverse: _*)
  }

  class Multiple private[Attrs] (val resetMask: Long, val applyMask: Long, val attrs: Attr*)
      extends Attrs {
    assert(attrs.length != 1)
    override def hashCode() = attrs.hashCode()
    override def equals(other: Any) = (this, other) match {
      case (lhs: Attr, rhs: Attr)                                    => lhs eq rhs
      case (lhs: Attr, rhs: Attrs.Multiple) if rhs.attrs.length == 1 => lhs eq rhs.attrs(0)
      case (lhs: Attrs.Multiple, rhs: Attr) if lhs.attrs.length == 1 => lhs.attrs(0) eq rhs
      case (lhs: Attrs.Multiple, rhs: Attrs.Multiple)                => lhs.attrs eq rhs.attrs
      case _                                                         => false
    }

    override def toString = s"Attrs(${attrs.mkString(",")})"

    def ++(other: fansi.Attrs) = Attrs(attrs ++ toSeq(other): _*)
  }
  def toSeq(attrs: Attrs) = attrs match {
    case m: Multiple => m.attrs
    case a: Attr     => Seq(a)
  }
}

/** Represents a single, atomic ANSI escape sequence that results in a color, background or
  * decoration being added to the output. May or may not have an escape sequence (`escapeOpt`), as
  * some attributes (e.g. [[Bold.Off]]) are not widely/directly supported by terminals and so
  * fansi.Str supports them by rendering a hard [[Attr.Reset]] and then re-rendering other [[Attr]]s
  * that are active.
  *
  * Many of the codes were stolen shamelessly from
  *
  * http://misc.flogisoft.com/bash/tip_colors_and_formatting
  */
sealed trait Attr extends Attrs {
  def attrs = Seq(this)

  /** escapeOpt the actual ANSI escape sequence corresponding to this Attr
    */
  def escapeOpt: Option[String]

  def name: String

  /** Combine this [[fansi.Attr]] with one or more other [[fansi.Attr]]s so they can be passed
    * around together
    */
  def ++(other: fansi.Attrs): Attrs = Attrs(Array(this) ++ Attrs.toSeq(other): _*)
}
object Attr {

  /** Represents the removal of all ansi text decoration. Doesn't fit into any convenient category,
    * since it applies to them all.
    */
  val Reset = new EscapeAttr(Console.RESET, Int.MaxValue, 0)

  /** A list of possible categories
    */
  val categories = Vector[Category](
    Color,
    Back,
    Bold,
    Underlined,
    Reversed
  )
}

/** An [[Attr]] represented by an fansi escape sequence
  */
case class EscapeAttr private[fansi] (escape: String, resetMask: Long, applyMask: Long)(implicit
  sourceName: sourcecode.Name
) extends Attr {
  val escapeOpt         = Some(escape)
  val name              = sourceName.value
  override def toString = escape + name + Console.RESET
}

/** An [[Attr]] for which no fansi escape sequence exists
  */
case class ResetAttr private[fansi] (resetMask: Long, applyMask: Long)(implicit
  sourceName: sourcecode.Name
) extends Attr {
  val escapeOpt         = None
  val name              = sourceName.value
  override def toString = name
}

/** Represents a set of [[fansi.Attr]]s all occupying the same bit-space in the state `Int`
  */
sealed abstract class Category(val offset: Int, val width: Int)(implicit catName: sourcecode.Name) {
  def mask = ((1 << width) - 1) << offset
  val all: Vector[Attr]

  def lookupEscape(applyState: Long) = {
    val escapeOpt = lookupAttr(applyState).escapeOpt
    if (escapeOpt.isDefined) escapeOpt.get
    else ""
  }
  def lookupAttr(applyState: Long) = lookupAttrTable((applyState >> offset).toInt)

  // Allows fast lookup of categories based on the desired applyState
  protected[this] def lookupTableWidth = 1 << width

  protected[this] lazy val lookupAttrTable = {
    val arr = new Array[Attr](lookupTableWidth)
    for (attr <- all)
      arr((attr.applyMask >> offset).toInt) = attr
    arr
  }

  def makeAttr(s: String, applyValue: Long)(implicit name: sourcecode.Name) =
    new EscapeAttr(s, mask, applyValue << offset)(catName.value + "." + name.value)

  def makeNoneAttr(applyValue: Long)(implicit name: sourcecode.Name) =
    new ResetAttr(mask, applyValue << offset)(catName.value + "." + name.value)
}

/** [[Attr]]s to turn text bold/bright or disable it
  */
object Bold extends Category(offset = 0, width = 1) {
  val On                = makeAttr(Console.BOLD, 1)
  val Off               = makeNoneAttr(0)
  val all: Vector[Attr] = Vector(On, Off)
}

/** [[Attr]]s to reverse the background/foreground colors of your text, or un-reverse them
  */
object Reversed extends Category(offset = 1, width = 1) {
  val On                = makeAttr(Console.REVERSED, 1)
  val Off               = makeAttr("\u001b[27m", 0)
  val all: Vector[Attr] = Vector(On, Off)
}

/** [[Attr]]s to enable or disable underlined text
  */
object Underlined extends Category(offset = 2, width = 1) {
  val On                = makeAttr(Console.UNDERLINED, 1)
  val Off               = makeAttr("\u001b[24m", 0)
  val all: Vector[Attr] = Vector(On, Off)
}

/** [[Attr]]s to set or reset the color of your foreground text
  */
object Color extends ColorCategory(offset = 3, width = 25, colorCode = 38) {

  val Reset        = makeAttr("\u001b[39m", 0)
  val Black        = makeAttr(Console.BLACK, 1)
  val Red          = makeAttr(Console.RED, 2)
  val Green        = makeAttr(Console.GREEN, 3)
  val Yellow       = makeAttr(Console.YELLOW, 4)
  val Blue         = makeAttr(Console.BLUE, 5)
  val Magenta      = makeAttr(Console.MAGENTA, 6)
  val Cyan         = makeAttr(Console.CYAN, 7)
  val LightGray    = makeAttr("\u001b[37m", 8)
  val DarkGray     = makeAttr("\u001b[90m", 9)
  val LightRed     = makeAttr("\u001b[91m", 10)
  val LightGreen   = makeAttr("\u001b[92m", 11)
  val LightYellow  = makeAttr("\u001b[93m", 12)
  val LightBlue    = makeAttr("\u001b[94m", 13)
  val LightMagenta = makeAttr("\u001b[95m", 14)
  val LightCyan    = makeAttr("\u001b[96m", 15)
  val White        = makeAttr("\u001b[97m", 16)

  val all: Vector[Attr] = Vector(
    Reset,
    Black,
    Red,
    Green,
    Yellow,
    Blue,
    Magenta,
    Cyan,
    LightGray,
    DarkGray,
    LightRed,
    LightGreen,
    LightYellow,
    LightBlue,
    LightMagenta,
    LightCyan,
    White
  ) ++ Full

}

/** [[Attr]]s to set or reset the color of your background
  */
object Back extends ColorCategory(offset = 28, width = 25, colorCode = 48) {

  val Reset        = makeAttr("\u001b[49m", 0)
  val Black        = makeAttr(Console.BLACK_B, 1)
  val Red          = makeAttr(Console.RED_B, 2)
  val Green        = makeAttr(Console.GREEN_B, 3)
  val Yellow       = makeAttr(Console.YELLOW_B, 4)
  val Blue         = makeAttr(Console.BLUE_B, 5)
  val Magenta      = makeAttr(Console.MAGENTA_B, 6)
  val Cyan         = makeAttr(Console.CYAN_B, 7)
  val LightGray    = makeAttr("\u001b[47m", 8)
  val DarkGray     = makeAttr("\u001b[100m", 9)
  val LightRed     = makeAttr("\u001b[101m", 10)
  val LightGreen   = makeAttr("\u001b[102m", 11)
  val LightYellow  = makeAttr("\u001b[103m", 12)
  val LightBlue    = makeAttr("\u001b[104m", 13)
  val LightMagenta = makeAttr("\u001b[105m", 14)
  val LightCyan    = makeAttr("\u001b[106m", 15)
  val White        = makeAttr("\u001b[107m", 16)

  val all: Vector[Attr] = Vector(
    Reset,
    Black,
    Red,
    Green,
    Yellow,
    Blue,
    Magenta,
    Cyan,
    LightGray,
    DarkGray,
    LightRed,
    LightGreen,
    LightYellow,
    LightBlue,
    LightMagenta,
    LightCyan,
    White
  ) ++ Full
}

/** An string trie for quickly looking up values of type [[T]] using string-keys. Used to speed up
  */
private[this] final class Trie[T](strings: Seq[(String, T)]) {

  val (min, max, arr, value) =
    strings.partition(_._1.isEmpty) match {
      case (Nil, continuations) =>
        val allChildChars = continuations.map(_._1(0))
        val min           = allChildChars.min
        val max           = allChildChars.max

        val arr = new Array[Trie[T]](max - min + 1)
        for ((char, ss) <- continuations.groupBy(_._1(0)))
          arr(char - min) = new Trie(ss.map { case (k, v) => (k.tail, v) })

        (min, max, arr, None)

      case (Seq((_, terminalValue)), Nil) =>
        (
          0.toChar,
          0.toChar,
          new Array[Trie[T]](0),
          Some(terminalValue)
        )

      case _ => ???
    }

  def apply(c: Char): Trie[T] =
    if (c > max || c < min) null
    else arr(c - min)

  /** Returns the length of the matching string, or -1 if not found
    */
  def query(input: CharSequence, index: Int): Option[(Int, T)] = {

    @tailrec def rec(offset: Int, currentNode: Trie[T]): Option[(Int, T)] =
      if (currentNode.value.isDefined) currentNode.value.map(offset - index -> _)
      else if (offset >= input.length) None
      else {
        val char = input.charAt(offset)
        val next = currentNode(char)
        if (next == null) None
        else rec(offset + 1, next)
      }
    rec(index, this)
  }
}

/** * Color a encoded on 25 bit as follow : 0 : reset value 1 - 16 : 3 bit colors 17 - 272 : 8 bit
  * colors 273 - 16 777 388 : 24 bit colors
  */
abstract class ColorCategory(offset: Int, width: Int, val colorCode: Int)(implicit
  catName: sourcecode.Name
) extends Category(offset, width)(catName) {

  /** 256 color [[Attr]]s, for those terminals that support it
    */
  val Full =
    for (x <- 0 until 256)
      yield makeAttr(s"\u001b[$colorCode;5;${x}m", 17 + x)(s"Full($x)")

  private[this] def True0(r: Int, g: Int, b: Int, index: Int) =
    makeAttr(trueRgbEscape(r, g, b), 273 + index)("True(" + r + "," + g + "," + b + ")")
  def trueRgbEscape(r: Int, g: Int, b: Int) =
    "\u001b[" + colorCode + ";2;" + r + ";" + g + ";" + b + "m"

  /** Create a TrueColor color, from a given index within the 16-million-color TrueColor range
    */
  def True(index: Int) = {
    require(
      0 <= index && index <= (1 << 24),
      "True parameter `index` must be 273 <= index <= 16777488, not " + index
    )
    val r = index >> 16
    val g = (index & 0x00ff00) >> 8
    val b = index & 0x0000ff
    True0(r, g, b, index)
  }

  /** Create a TrueColor color, from a given (r, g, b) within the 16-million-color TrueColor range
    */
  def True(r: Int, g: Int, b: Int) = True0(r, g, b, trueIndex(r, g, b))

  def trueIndex(r: Int, g: Int, b: Int) = {
    require(0 <= r && r < 256, "True parameter `r` must be 0 <= r < 256, not " + r)
    require(0 <= g && g < 256, "True parameter `g` must be 0 <= r < 256, not " + g)
    require(0 <= b && b < 256, "True parameter `b` must be 0 <= r < 256, not " + b)
    r << 16 | g << 8 | b
  }

  override def lookupEscape(applyState: Long) = {
    val rawIndex = (applyState >> offset).toInt
    if (rawIndex < 273) super.lookupEscape(applyState)
    else {
      val index = rawIndex - 273
      trueRgbEscape(r = index >> 16, g = (index & 0x00ff00) >> 8, b = index & 0x0000ff)
    }
  }
  override def lookupAttr(applyState: Long): Attr = {
    val index = (applyState >> offset).toInt
    if (index < 273) lookupAttrTable(index)
    else True(index - 273)

  }
  override protected[this] def lookupTableWidth = 273
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy