11
\$\begingroup\$

Terminals are so boring these days. They used to look like this:

Terminal 1 Terminal 2

Now they're just bland and dull and black-and-white. I want you to write me a program that will make my terminal all colorful again!

Description

Take this example Ruby code:

Example code

Most Linux terminals support these escape sequences (\e stands for the escape character), and Windows can support them with ANSICON. Here is the syntax of the specific escape sequence that can change the text or background color of a string:

\e[{{COLOR}}m

where \e stands for the escape character (0x1B in ASCII) and {{COLOR}} is replaced by the number of the color that you want to use (more details on that later). Text that comes after this escape sequence will be formatted as directed, and a value of 0 will reset all formatting.

Your challenge is to take a string specifying some text that may contain color, and output a colorful version of it.

Input / Output

Normal text works just like normal, and is printed literally. For example, the input waffles yields the same output, with no special color.

The syntax for specifying a color is similar to Wikipedia's syntax. For example, to color the words "the color red" in red in the sentence This is the color red!, the input would be:

This is {{red|the color red}}!

Background colors work too. If you wanted black letters on a white background, you would use this:

{{black|white|This text is black on white}}

To get only a background color, omit the foreground:

{{|red|This text has a red background}}

Specification

Two open curly brackets always specify the beginning of a color directive. Two closing curly brackets specify the end. Brackets will always match; there will never be a {{ without a corresponding }}, and a }} will never come before its corresponding {{. These color directives will not be nested, and a {{ will never appear within a color directive.

Within a color directive, there will always be either one or two | symbols. If there is one, the text before it is the foreground color and the text after is the string to show in that color. If there are two, the text before the first one is the foreground color, the text after the first but before the second is the background color, and the text after the second is the string to display. These vertical bars may exist outside of a color directive, and should be printed literally.

The foreground color or background color (but not both) may be empty, in which case you should leave them as the default. The final string (the one to output) will never be empty.

Here are the directions to output text of a certain color:

  • A color sequence is defined in the "Description" section. For example, a color sequence of 42 would be "\e[42m".

  • To set a color, print the color sequence of the number determined below:

     Color name   | Color sequence number (foreground / background)
    --------------+----------
     black        | 30 / 40
     red          | 31 / 41
     green        | 32 / 42
     yellow       | 33 / 43
     blue         | 34 / 44
     magenta      | 35 / 45
     cyan         | 36 / 46
     lightgray    | 37 / 47
     darkgray     | 90 / 100
     lightred     | 91 / 101
     lightgreen   | 92 / 102
     lightyellow  | 93 / 103
     lightblue    | 94 / 104
     lightmagenta | 95 / 105
     lightcyan    | 96 / 106
     white        | 97 / 107
    
  • Color names are case sensitive, and an invalid color name will never be provided. You don't have to handle stuff like RED or lightgrey (spelled with an e).

  • After you print a color sequence, it will apply for all text following it. To end a color sequence (reset to the default color), output a color sequence of 0 ("\e[0m").

Test case

 {{|yellow|     }}
{{|yellow| }}     {{|yellow| }}
{{|yellow| }} {{red|'}} {{red|'}} {{|yellow| }}
{{|yellow| }} \_/ {{|yellow| }}
{{|yellow| }}     {{|yellow| }}
 {{|yellow|     }}

This should output a smiley face... with evil red eyes.

Rules

  • You may not use any libraries or functions of your programming language to automatically parse a color. This means that you must be the one to determine what "red" means; you can't have a library automatically do that for you.

  • This is , so the shortest code in bytes will win!

\$\endgroup\$
7
  • \$\begingroup\$ It is actually supposed to be a terminal? Or just a colorful text viewer? Is it supposed to run commands? \$\endgroup\$ Aug 29, 2014 at 22:50
  • \$\begingroup\$ I'm finding it hard to test this. Everything I send to STDOUT using the specified syntax comes in plain text. My bash profile uses a colored prompt, so stealing from that I tried \n\[\e[32m\]\w\n\[\e[0m\]> (green directory name, plain prompt on next line), but I can't get it to work from a program (tried python and Java so far). Any ideas? \$\endgroup\$
    – Geobits
    Aug 29, 2014 at 23:03
  • \$\begingroup\$ @Geobits Try echo -e "\e[31mtest\e[0m". \$\endgroup\$
    – Doorknob
    Aug 29, 2014 at 23:05
  • 4
    \$\begingroup\$ I think you would enjoy lolcat. \$\endgroup\$ Aug 30, 2014 at 22:05
  • 1
    \$\begingroup\$ I think that by you he figuratively means your program (as opposed to a call to a library function), and that he takes determine in the sense of figure out, not as in choose. Ie, it is your program that should handle the mapping: String("red") |-> Integer(31). red is only 31 because he says so, that information needs to be integrated into the program. Although it may be argued exactly what would count as your program - can we use general-purpose String manipulation functions? - don't blatantly cheat/abuse. \$\endgroup\$
    – blutorange
    Aug 31, 2014 at 22:31

6 Answers 6

7
\$\begingroup\$

Ruby, 205 189 188 186 185 182 174 170 165 161 159 154 bytes

Putting long strings of color names in your code doesn't look nerdy enough.

Down to 170 partly thanks to rubik. Now the scrolling bars are gone!

One obvious and one not so obvious improvement, thanks to the flex answer, without the improvement to it, I wouldn't have revisited this!

Not anymore, I saved 4 bytes with #sum. I didn't intend it, but I just noticed this solution is case-insensitive as well. It happily processes {{RED|Red text}}.

Hex dump:

0000000: 7a3d 2d3e 6a7b 693d 2240 3054 2d44 1547  z=->j{i="@0T-D.G
0000010: 5155 0034 3256 2f46 1749 0b22 2e69 6e64  QU.42V/F.I.".ind
0000020: 6578 2028 415b 6a5d 2e74 6f5f 732e 7375  ex (A[j].to_s.su
0000030: 6d25 3839 292e 6368 723b 692b 3d69 3e39  m%89).chr;i+=i>9
0000040: 3f38 303a 3330 7d0a 243e 3c3c 243c 2e72  ?80:30}.$><<$<.r
0000050: 6561 642e 6773 7562 282f 7b7b 282e 2a3f  ead.gsub(/{{(.*?
0000060: 297d 7d2f 297b 413d 2431 2e73 706c 6974  )}}/){A=$1.split
0000070: 277c 273b 221b 5b25 693b 2569 6d23 7b41  '|';".[%i;%im#{A
0000080: 2e70 6f70 7d1b 5b30 6d22 255b 7a5b 305d  .pop}.[0m"%[z[0]
0000090: 2c31 302b 7a5b 315d 5d7d                 ,10+z[1]]}

You can convert it with xxd -r hex.dump.

The program with all non-printable characters escaped for reference purposes:

z=->j{i="@0T-D\x15GQU\x0042V/F\x17I\v".index (A[j].to_s.sum%89).chr;i+=i>9?80:30}
$><<$<.read.gsub(/{{(.*?)}}/){A=$1.split'|';"\x1b[%i;%im#{A.pop}\x1b[0m"%[z[0],10+z[1]]}

That is one line. Use it like this

ruby colors.rb -W0 < input.txt

The -W0 flag suppresses warnings that would be sent to stderr otherwise. However, the program works fine without any flags.

Output:

output

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Ah, I had the same idea, but you beat me to it! I think you could save a char with base 35, modulus 98 and xor 1. The string would be: '1?IYU_N[(\x0c\x16&",\x1f\x01'. My string is 16 in length though. I see yours is 18, so you probably have to adjust. \$\endgroup\$
    – rubik
    Sep 1, 2014 at 7:46
  • \$\begingroup\$ Thanks. The two addtional bytes are there to support the color code 39/49, which sets the default back/forground color. But thanks for the tip, I'm currently at work and I'll give it some more thought when I get back home. \$\endgroup\$
    – blutorange
    Sep 1, 2014 at 13:24
  • 1
    \$\begingroup\$ Well I observed that the only bases you can use are 35 and 36 (at least Python's int() function cannot go over 36). Then I tried all the combinations for the modulus (from 2 to 10000, but in theory one could widen the search to all of Unicode) and for the xor, which I kept small (1 to 9). Then I considered acceptable results only those which didn't contain duplicate characters. \$\endgroup\$
    – rubik
    Sep 1, 2014 at 18:32
  • \$\begingroup\$ Yes, that is more or less what I did as well. Originally, I was limiting myself to printable characters, because that gives you less of a headache and looks nicer. But since I am using the byte 0x1e instead of the escape sequence already, I might as well use more non-printable characters. In order to achieve printable characters, I used x.to_i(base)%mod+offset. I then replaced the + with ^, because well, it looks cooler. Other than that, it is unneccessary. Dropping the ^99 and changing << to + saved for more bytes. Thanks for the tip, I wouldn't have noticed otherwise! \$\endgroup\$
    – blutorange
    Sep 1, 2014 at 22:32
4
\$\begingroup\$

Ruby, 329 bytes.

h={};(y=0..15).each{|i|h[%w(black red green yellow blue magenta cyan lightgray darkgray lightred lightgreen lightyellow lightblue lightmagenta lightcyan white)[i]]=y.map{|j|[30,40].map{|k|k+i%8+i/8*60}}[i]}
loop{puts gets.gsub(/{{.+?}}/){|x|"\e[#{h[(g=x.scan(/[^{}|]+/))[0]][0]}m#{(g[2]? "\e[#{h[g[1]][1]}m":'')}#{g.last}\e[0m"}}
\$\endgroup\$
4
  • \$\begingroup\$ What version of Ruby do I need to run this? I used ruby 2.1.2p95 and it throw error: undefined method 'gsub' for nil:NilClass (NoMethodError) . \$\endgroup\$
    – Ray
    Aug 30, 2014 at 22:38
  • \$\begingroup\$ Hi @Ray, it works in 2.0.0-p451. I didn't try it in 2.1.2. Here it works as a script and Here it works in irb. \$\endgroup\$
    – Alex Deva
    Aug 31, 2014 at 6:55
  • \$\begingroup\$ It works when you enter text manually. If you do ruby colors.rb < input.txt, it will keep looping after all the input has been read. Then gets returns nil, which doesn't possess a #gsub method, thus raising an error. Use $><<$<.read instead of loop{puts gets, it's also shorter ; ) \$\endgroup\$
    – blutorange
    Aug 31, 2014 at 13:32
  • \$\begingroup\$ I just tested this script with the smiley face (see question and the image from my post), and there's no yellow border around the smiley? \$\endgroup\$
    – blutorange
    Aug 31, 2014 at 13:39
4
\$\begingroup\$

Flex (lexer) - 226 197 192 182 168 (or 166)

To get it down to 166, change the \33 to an actual escape character.

 int z;p(i){printf("\33[%dm",i);}
%%
"{{" z=2;
[a-z]*\| if(!z)REJECT;~-yyleng&&p("062q00t03058ns7uo0p90r4"[*(int*)&yytext[yyleng>7?4:0]%131%27]-10*z);z--;
"}}" p(z=0);

Compile and run:

$ flex -o colour.c colour.l
$ gcc -o colour colour.c -lfl
$ ./colour < input
\$\endgroup\$
3
\$\begingroup\$

Python - 351

import re,sys
R=range
E=lambda n,d=0:'\033[%dm'%(T[n]+d)if n else''
def P(m):f,b,t=m.groups();return'%s%s%s\033[0m'%(E(f),E(b,10),t)
x='!red!green!yellow!blue!magenta!cyan'.replace
T=dict(zip(('black'+x('!',' ')+' lightgray darkgray'+x('!',' light')+' white').split(),R(30,38)+R(90,98)))
print re.sub(r'{{(\w+)?\|?(\w+)?\|?(.+?)}}',P,sys.stdin.read())
\$\endgroup\$
1
\$\begingroup\$

Cobra - 496

It could almost be a single print statement.

use System.Text.RegularExpressions
class P
    def main
        print Regex.replace(Console.readLine,r'\{\{('+(l=List<of String>(((m=' black red green yellow blue magenta cyan'.split).join(' ')+' lightgray darkgray'+m.join(' light')+' white').split))[1:].join('|')+r')?\|?('+l[1:].join('|')+r')?\|(.*?)\}\}',do(a as Match))
            return if(x=l.indexOf('[a.groups[1]]'),r'\e['+'[if(x>8,x+81,x+29)]m','')+if(y=l.indexOf('[a.groups[2]]'),r'\e['+'[if(y>8,y+91,y+39)]m','')+'[a.groups[3]]'+if(x+y,r'\e[0m','')
\$\endgroup\$
1
\$\begingroup\$

Python, 561

Reads the text to format from stdin.

import re,sys
def p(f,b,t):
    p=''
    m='\033[%dm'
    if f!=0:p+=m%f
    if b!=0:p+=m%b
    return p+t+m%0
def c(n,b=0):
    s='black:30#red:31#green:32#yellow:33#blue:34#magenta:35#cyan:36#lightgray:37#darkgray:90#lightred:91#lightgreen:92#lightyellow:93#lightblue:94#lightmagenta:95#lightcyan:96#white:97'
    r=0
    for i in s.split('#'):
        (t,c)=i.split(':')
        if t==n:
            r=int(c)
            if b==1:r+=10
    return r
def r(m):
    i=m.groups()
    f=b=0
    if i[0]!='':f=c(i[0])
    if i[1]!=None:b=c(i[1],1)
    return p(f,b,i[2])
print re.sub('{{(\w*)\|(?:(\w*)\|)?([^}]+)}}',r,sys.stdin.read())
\$\endgroup\$
6
  • 2
    \$\begingroup\$ It's too verbose to have is not None in a codegolf. You may use !=None, for example. \$\endgroup\$
    – Ray
    Aug 30, 2014 at 21:38
  • \$\begingroup\$ Also, in def p(f,b,t), your code will throw a ZeroDivisionError. Anything mod 0 is impossible. \$\endgroup\$
    – Beta Decay
    Aug 31, 2014 at 12:17
  • \$\begingroup\$ @BetaDecay that's not an integer being modulus'ed, that's a string being formatted. \$\endgroup\$
    – Sammitch
    Sep 2, 2014 at 16:35
  • \$\begingroup\$ I'm getting invalid syntax errors at re.sub running this \$\endgroup\$
    – ArtOfCode
    Jan 22, 2015 at 22:09
  • \$\begingroup\$ (late comment) Does this 499-byte code work? \$\endgroup\$ Jul 20, 2016 at 13:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.