File: coma.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 2020-2025 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the “Software”), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 /*
  26 Single-file source-code for coma.
  27 
  28 To compile a smaller-sized command-line app, you can use the `go` command as
  29 follows:
  30 
  31 go build -ldflags "-s -w" -trimpath coma.go
  32 */
  33 
  34 package main
  35 
  36 import (
  37     "bufio"
  38     "bytes"
  39     "os"
  40     "regexp"
  41 )
  42 
  43 var styleAliases = map[string]string{
  44     `b`: `blue`,
  45     `g`: `green`,
  46     `m`: `magenta`,
  47     `o`: `orange`,
  48     `p`: `purple`,
  49     `r`: `red`,
  50     `u`: `underline`,
  51 
  52     `bb`: `blueback`,
  53     `bg`: `greenback`,
  54     `bm`: `magentaback`,
  55     `bo`: `orangeback`,
  56     `bp`: `purpleback`,
  57     `br`: `redback`,
  58 
  59     `gb`: `greenback`,
  60     `mb`: `magentaback`,
  61     `ob`: `orangeback`,
  62     `pb`: `purpleback`,
  63     `rb`: `redback`,
  64 
  65     `hi`:  `inverse`,
  66     `inv`: `inverse`,
  67     `mag`: `magenta`,
  68 
  69     `du`: `doubleunderline`,
  70 
  71     `flip`: `inverse`,
  72     `swap`: `inverse`,
  73 
  74     `reset`:     `plain`,
  75     `highlight`: `inverse`,
  76     `hilite`:    `inverse`,
  77     `invert`:    `inverse`,
  78     `inverted`:  `inverse`,
  79     `swapped`:   `inverse`,
  80 
  81     `dunderline`:  `doubleunderline`,
  82     `dunderlined`: `doubleunderline`,
  83 
  84     `strikethrough`: `strike`,
  85     `strikethru`:    `strike`,
  86     `struck`:        `strike`,
  87 
  88     `underlined`: `underline`,
  89 
  90     `bblue`:    `blueback`,
  91     `bgray`:    `grayback`,
  92     `bgreen`:   `greenback`,
  93     `bmagenta`: `magentaback`,
  94     `borange`:  `orangeback`,
  95     `bpurple`:  `purpleback`,
  96     `bred`:     `redback`,
  97 
  98     `bgblue`:    `blueback`,
  99     `bggray`:    `grayback`,
 100     `bggreen`:   `greenback`,
 101     `bgmag`:     `magentaback`,
 102     `bgmagenta`: `magentaback`,
 103     `bgorange`:  `orangeback`,
 104     `bgpurple`:  `purpleback`,
 105     `bgred`:     `redback`,
 106 
 107     `bluebg`:    `blueback`,
 108     `graybg`:    `grayback`,
 109     `greenbg`:   `greenback`,
 110     `magbg`:     `magentaback`,
 111     `magentabg`: `magentaback`,
 112     `orangebg`:  `orangeback`,
 113     `purplebg`:  `purpleback`,
 114     `redbg`:     `redback`,
 115 
 116     `backblue`:    `blueback`,
 117     `backgray`:    `grayback`,
 118     `backgreen`:   `greenback`,
 119     `backmag`:     `magentaback`,
 120     `backmagenta`: `magentaback`,
 121     `backorange`:  `orangeback`,
 122     `backpurple`:  `purpleback`,
 123     `backred`:     `redback`,
 124 }
 125 
 126 var styles = map[string]string{
 127     `blue`:            "\x1b[38;2;0;95;215m",
 128     `bold`:            "\x1b[1m",
 129     `doubleunderline`: "\x1b[21m",
 130     `gray`:            "\x1b[38;2;168;168;168m",
 131     `green`:           "\x1b[38;2;0;135;95m",
 132     `inverse`:         "\x1b[7m",
 133     `magenta`:         "\x1b[38;2;215;0;255m",
 134     `orange`:          "\x1b[38;2;215;95;0m",
 135     `plain`:           "\x1b[0m",
 136     `purple`:          "\x1b[38;2;135;95;255m",
 137     `red`:             "\x1b[38;2;204;0;0m",
 138     `strike`:          "\x1b[9m",
 139     `underline`:       "\x1b[4m",
 140 
 141     `blueback`:    "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
 142     `grayback`:    "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
 143     `greenback`:   "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
 144     `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
 145     `orangeback`:  "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
 146     `purpleback`:  "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
 147     `redback`:     "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
 148 }
 149 
 150 type patternStylePair struct {
 151     expr  *regexp.Regexp
 152     style string
 153 }
 154 
 155 func main() {
 156     if len(os.Args)%2 != 1 {
 157         const msg = `you forgot the style-name for/after the last regex`
 158         os.Stderr.WriteString("\x1b[31m")
 159         os.Stderr.WriteString(msg)
 160         os.Stderr.WriteString("\x1b[0m\n")
 161         os.Exit(1)
 162     }
 163 
 164     nerr := 0
 165     pairs := make([]patternStylePair, 0, len(os.Args[1:])/2)
 166 
 167     for args := os.Args[1:]; len(args) >= 2; args = args[2:] {
 168         resrc := args[0]
 169         sname := args[1]
 170 
 171         e, err := regexp.Compile(resrc)
 172         if err != nil {
 173             os.Stderr.WriteString("\x1b[31m")
 174             os.Stderr.WriteString(err.Error())
 175             os.Stderr.WriteString("\x1b[0m\n")
 176             nerr++
 177         }
 178 
 179         if alias, ok := styleAliases[sname]; ok {
 180             sname = alias
 181         }
 182 
 183         style, ok := styles[sname]
 184         if !ok {
 185             os.Stderr.WriteString("\x1b[31mno style named `")
 186             os.Stderr.WriteString(args[1])
 187             os.Stderr.WriteString("`\x1b[0m\n")
 188             nerr++
 189         }
 190 
 191         pairs = append(pairs, patternStylePair{expr: e, style: style})
 192     }
 193 
 194     if nerr > 0 {
 195         os.Exit(1)
 196     }
 197 
 198     sc := bufio.NewScanner(os.Stdin)
 199     sc.Buffer(nil, 8*1024*1024*1024)
 200     bw := bufio.NewWriter(os.Stdout)
 201 
 202     for sc.Scan() {
 203         handleLine(bw, sc.Bytes(), pairs)
 204         bw.WriteByte('\n')
 205         if err := bw.Flush(); err != nil {
 206             return
 207         }
 208     }
 209 }
 210 
 211 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 212 // either the alert/bell byte, or the multi-byte sequences starting either
 213 // with ESC[ or ESC]; either returned index can be negative
 214 func indexEscapeSequence(s []byte) (int, int) {
 215     var prev byte
 216 
 217     for i, b := range s {
 218         if b == '\a' {
 219             return i, i + 1
 220         }
 221 
 222         if prev == '\x1b' && b == '[' {
 223             j := indexLetter(s[i+1:])
 224             if j < 0 {
 225                 return i, -1
 226             }
 227             return i - 1, i + 1 + j + 1
 228         }
 229 
 230         if prev == '\x1b' && b == ']' {
 231             j := bytes.IndexByte(s[i+1:], ':')
 232             if j < 0 {
 233                 return i, -1
 234             }
 235             return i - 1, i + 1 + j + 1
 236         }
 237 
 238         if prev == '\x1b' && b == '\\' {
 239             return i - 1, i + 1
 240         }
 241 
 242         prev = b
 243     }
 244 
 245     return -1, -1
 246 }
 247 
 248 func indexLetter(s []byte) int {
 249     for i, b := range s {
 250         if 'A' <= b && b <= 'Z' {
 251             return i
 252         }
 253         if 'a' <= b && b <= 'z' {
 254             return i
 255         }
 256     }
 257 
 258     return -1
 259 }
 260 
 261 func handleLine(w *bufio.Writer, s []byte, with []*regexp.Regexp) {
 262     for len(s) > 0 {
 263         i, j := indexEscapeSequence(s)
 264         if i < 0 {
 265             handleLineChunk(w, s, with)
 266             return
 267         }
 268 
 269         handleLineChunk(w, s[:i], with)
 270         w.Write(s[i:j])
 271 
 272         if j < 0 {
 273             break
 274         }
 275         s = s[j:]
 276     }
 277 }
 278 
 279 func handleLineChunk(w *bufio.Writer, s []byte, with []patternStylePair) {
 280     start := -1
 281     end := -1
 282     which := -1
 283 
 284     for len(s) > 0 {
 285         start = -1
 286         for i, pair := range with {
 287             span := pair.expr.FindIndex(s)
 288             if span != nil && (span[0] < start || start < 0) {
 289                 start = span[0]
 290                 end = span[1]
 291                 which = i
 292             }
 293         }
 294 
 295         if start < 0 {
 296             w.Write(s)
 297             return
 298         }
 299 
 300         w.Write(s[:start])
 301         w.WriteString(with[which].style)
 302         w.Write(s[start:end])
 303         w.WriteString("\x1b[0m")
 304         s = s[end:]
 305     }
 306 }