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

replpp.shaded.fansi.Fansi.scala Maven / Gradle / Ivy

There is a newer version: 0.1.98
Show newest version
package replpp.shaded.fansi
import replpp.shaded.{fansi, sourcecode}

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
  */
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)

    new 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

  /** Shorthand constructor with ErrorMode.Sanitize */
  def Sanitize(raw: CharSequence) = apply(raw, ErrorMode.Sanitize)

  /** Shorthand constructor with ErrorMode.Strip */
  def Strip(raw: CharSequence) = apply(raw, ErrorMode.Strip)

  /** Shorthand constructor with ErrorMode.Throw */
  def Throw(raw: CharSequence) = apply(raw, ErrorMode.Throw)
  /**
    * 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
      }
    }

    new 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 apply(args: Str*): fansi.Str = {
    join(args)
  }
  def join(args: Iterable[Str], sep: fansi.Str = fansi.Str("")) = {
    val length = args.iterator.map(_.length + sep.length).sum - sep.length
    val chars = new Array[Char](length)
    val colors = new Array[State](length)
    var j = 0
    for (arg <- args){

      if (j != 0){
        var k = 0
        while (k < sep.length){
          chars(j) = sep.getChar(k)
          colors(j) = sep.getColors(k)
          j += 1
          k += 1
        }
      }
      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