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 func findANSI(s []byte) (int, int) {
 212     var prev byte
 213 
 214     for i, b := range s {
 215         if prev != '\x1b' {
 216             prev = b
 217             continue
 218         }
 219 
 220         if b == '[' {
 221             for j, b := range s[i+1:] {
 222                 if ('A' <= b && b <= 'Z') || ('a' <= b && b <= 'z') {
 223                     return i - 1, i + 1 + j + 1
 224                 }
 225             }
 226             return i - 1, len(s)
 227         }
 228 
 229         if b == ']' {
 230             j := bytes.IndexByte(s[i+1:], '\a')
 231             if j < 0 {
 232                 return i - 1, len(s)
 233             }
 234             return i - 1, i + 1 + j + 1
 235         }
 236 
 237         prev = b
 238     }
 239 
 240     return -1, -1
 241 }
 242 
 243 func handleLine(w *bufio.Writer, s []byte, with []patternStylePair) {
 244     for len(s) > 0 {
 245         i, j := findANSI(s)
 246         if i < 0 {
 247             handleLineChunk(w, s, with)
 248             return
 249         }
 250 
 251         handleLineChunk(w, s[:i], with)
 252         w.Write(s[i:j])
 253         s = s[j:]
 254     }
 255 }
 256 
 257 func handleLineChunk(w *bufio.Writer, s []byte, with []patternStylePair) {
 258     start := -1
 259     end := -1
 260     which := -1
 261 
 262     for len(s) > 0 {
 263         start = -1
 264         for i, pair := range with {
 265             span := pair.expr.FindIndex(s)
 266             if span != nil && (span[0] < start || start < 0) {
 267                 start = span[0]
 268                 end = span[1]
 269                 which = i
 270             }
 271         }
 272 
 273         if start < 0 {
 274             w.Write(s)
 275             return
 276         }
 277 
 278         w.Write(s[:start])
 279         w.WriteString(with[which].style)
 280         w.Write(s[start:end])
 281         w.WriteString("\x1b[0m")
 282         s = s[end:]
 283     }
 284 }