Community discussions

MikroTik App
 
User avatar
Amm0
Forum Guru
Forum Guru
Topic Author
Posts: 4611
Joined: Sun May 01, 2016 7:12 pm
Location: California
Contact:

$CHALK - function for colorizing text output using ANSI codes

Mon Jul 17, 2023 12:13 am

I'd previous used the "/terminal style" commands to add colorized output from a script. The alternative method is encoding ANSI control codes into a string, so wrote a function that "wraps" some text with the needed escape to set text and background colors. The code is loosely based on a JavaScript/node project called "chalk.js", see https://github.com/chalk/chalk - although this code only deals with 8-bit colors (e.g. no RGB codes).

The $CHALK function can be used inline/interpolation to add a <color> to text in a string like so:
:put "$[$CHALK blue]hello world$[$CHALK no-style]"
Note: The colors need to be cleared to avoid the terminal using the last color set, this is done via "$CHALK no-style" which is included in-line above.


Or, If <text> is provided as the 2nd argument to $CHALK function, it will be output via a :put in one-line.
$CHALK red "hello world" bold=yes inverse=yes

Another use case is using debug=yes as an option $CHALK, this will output the ANSI escape code formatted for use in a Mikrotik script – e.g. you can use $CHALK to make an ANSI string WITHOUT having CHALK function load "at runtime".
[amm0@Mikrotik] /> $CHALK red debug=yes

\1B[31;49m

[amm0@Mikrotik] /> $CHALK no-style debug=yes

\1B[39;49m

[amm0@Mikrotik] /> :put "\1B[31;49mcut-and-pasted-codes\1B[39;49m"
cut-and-pasted-codes   # (in bold red)

The available color are available using "$CHALK colors":
Image


As a bonus, there a "url" option that will generate a "clickable" URL in a modern terminal:
     $CHALK url "http://example.com" text="Example Link"
Note: this does NOT work in the RouterOS terminal form winbox/webfig – however if you access the router using a SSH client, it create a clickable link with the URL in a Mac/Linux/WSL terminal.

Image


To see some examples, you can use "$CHALK help"...

The actual code for the $CHALK function that will be have to add to the RouterOS, either by cut-and-paste to test, or via /system/script or /system/scheduler so it's always loaded:

CHALK colorizing function code


:global CHALK do={
    # we may call ourselves for control codes, so declare that
    :global CHALK
    :local helptext "\
    \r\n \$CHALK
    \r\n  generates ANSI codes that can be used in a string to add colorized text\
    \r\n \
    \r\n Basic Syntax:\
    \r\n     \$CHALK <text-color> [<text>] [inverse=yes] [[bold=yes]|[dim=yes]]\
    \r\n \
    \r\n Alternatively, use set background (bg=) color, instead of inverse=yes:\
    \r\n     \$CHALK <text-color> [<text>] bg=<text-color> [[bold=yes]|[dim=yes]]\
    \r\n \
    \r\n View possible values of <text-color>:\
    \r\n     \$CHALK colors\
    \r\n \
    \r\n Clear all ANSI formatting:\
    \r\n     \$CHALK reset\
    \r\n \
    \r\n Clear only foreground and background colors:\
    \r\n     \$CHALK no-style\
    \r\n \
    \r\n Generate a \"clickable\" URL (in select terminals only):\
    \r\n     \$CHALK url \"http://example.com\" text=\"Example Link\"\
    \r\n \
    \r\n To see this page, use:\
    \r\n     \$CHALK help\
    \r\n \
    \r\n Example: \
    \r\n     Print (\"put\") some text in cyan -\
    \r\n         \$CHALK cyan \"hello world\" \
    \r\n \
    \r\n     Output blue text inside a string -\
    \r\n         :put \"\$[\$CHALK blue]hello world\$[\$CHALK no-style]\"\
    \r\n \
    \r\n     Shout bold text with background color (using inverse=yes) -\
    \r\n         :put \"\$[\$CHALK red inverse=yes bold=yes]HELLO WORLD\$[\$CHALK no-style]\"\
    \r\n \
    \r\n     Create a click-able URL -\
    \r\n         :put \"\$[\$CHALK url \"http://www.mikrotik.com\" text=\"Go to Mikrotik Website\"]\" \
    \r\n             ** only works when connected via SSH & using \"modern\" terminal\
    \r\n \
    \r\n     Show example colors -\
    \r\n         \$CHALK colors \
    \r\n "    
    
    # handle 8-bit color names
    :local lookupcolor8 do={
        :local color8 {
            black={30;40};
            red={31;41};
            green={32;42};
            yellow={33;43};
            blue={34;44};
            magenta={35;45};
            cyan={36;46};
            white={37;47};
            "no-style"={39;49};
            reset={0;0};
            "bright-black"={90;0};
            "gray"={90;100};
            "grey"={90;100};
            "bright-red"={91;101};
            "bright-green"={92;103};
            "bright-yellow"={93;104};
            "bright-blue"={94;104};
            "bright-magenta"={95;105};
            "bright-cyan"={96;106};
            "bright-white"={97;107}
        }
        :if ($1 = "as-array") do={:return $color8}
        :if ([:typeof ($color8->$1)]="array") do={
            :return ($color8->$1) 
        } else={
            :return [:nothing]
        }
    }

    :if ($1 = "color") do={
        :if ([:typeof $2] = "str") do={
            :local ccode [$lookupcolor8 $2]
            :if ([:len $ccode] > 0) do={
                :put $ccode 
                :return [:nothing]
            } else={$CHALK colors}
        } else={$CHALK colors}
    }
    :if ($1 = "colors") do={
        :put "\t <color>\t\t $[$CHALK no-style inverse=yes]inverse=yes$[$CHALK reset]\t\t $[$CHALK no-style bold=yes]bold=yes$[$CHALK reset]\t\t $[$CHALK no-style dim=yes]dim=yes$[$CHALK reset]"
        :foreach k,v in=[$lookupcolor8 as-array] do={
            :local ntabs "\t"
            :if ([:len $k] <  8 ) do={
                :set ntabs "\t\t"
            } 
            :put "\t$[$CHALK $k]$k$[$CHALK reset]$ntabs$[$CHALK $k inverse=yes]\t$k$[$CHALK reset]\t$[$CHALK $k bold=yes]$ntabs$k$[$CHALK reset]\t$[$CHALK $k dim=yes]$ntabs$k$[$CHALK reset]"

       } 
       :return [:nothing]
    }

    :if ($1 = "help") do={
        :put $helptext
        :return [:nothing]
    }

    # handle clickable URLs
    :if ($1 = "url") do={
        :local lurl "http://example.com"
        :if ([:typeof $2]="str") do={
            :set lurl $2
        } else={
            :if ([:typeof $url]="str") do={
                :set lurl $url
            } 
        }
        :local ltxt $lurl
        :if ([:typeof $text]="str") do={
            :set ltxt $text
        }
        :return "\1B]8;;$lurl\07$ltxt\1B]8;;\07" 
    }

    # set default colors
    :local c8str {mod="";fg="$([$lookupcolor8 no-style]->0)";bg="$([$lookupcolor8 no-style]->1)"}
    
    # if the color name is the 1st arg, make the the foreground color
    :if ([:typeof [$lookupcolor8 $1]] = "array") do={
        :set ($c8str->"fg") ([$lookupcolor8 $1]->0) 
    } 

    # set default colors
    
    # set the modifier...
    # hidden= 
    :if ($hidden="yes") do={
        :set ($c8str->"mod") "8;"
    } else={
        # inverse= 
        :if ($inverse="yes") do={
            :set ($c8str->"mod") "7;"
        } 
        # bold=
        :if ($bold="yes") do={
            :set ($c8str->"mod") "$($c8str->"mod")1;"
            # set both bold=yes and light=yes? bold wins...
        } else={
            # dim=
            :if ($dim="yes") do={
                :set ($c8str->"mod") "$($c8str->"mod")2;"
            }
        }        
    }

    # if bg= set, apply color  
    :if ([:typeof $bg]="str") do={
        :if ([:typeof [$lookupcolor8 $bg]] = "array") do={
            :set ($c8str->"bg") ([$lookupcolor8 $bg]->1)
        } else={:error "bg=$bg is not a valid color"}
    }
    
    # build the output
    :local rv "\1B[$($c8str->"mod")$($c8str->"fg");$($c8str->"bg")m"

    # if debug=yes, show the ANSI codes instead
    :if ($debug = "yes") do={
        :return [:put "\\1B[$[:pick $rv 2 80]"]
    }

    # if the 2nd arg is text, or text= set, 
    :local ltext $2
    :if ([:typeof $text]="str") do={
        :set ltext $text
    }
    
    :if ([:typeof $ltext] = "str") do={
        :return [:put "$rv$2$[$CHALK reset]"]
    }


    :return $rv
}
The following GitHub gist was used as reference for the ANSI codes used here: https://gist.github.com/fnky/458719343a ... a4f7296797

TODO
- support "cls" and/or"clear-screen" to wipe screen
- reset should be just ]0m not ]0,49m
- debug=yes does not work with URL nor includes text if there was text provided
- add RGB support if an 3 element array is provided {R;G;B} as a color name
- should support ascii name for control codes (e.g. $CHALK esc or $CHALK bel)
- incorporate some "color prefix code" like + - etc to avoid needing inverse= dim=
- help should use $0 instead of assuming name is CHALK


edit 1/2: updated TODOs
Last edited by Amm0 on Wed Jul 19, 2023 4:35 pm, edited 2 times in total.
 
User avatar
Kentzo
Long time Member
Long time Member
Posts: 653
Joined: Mon Jan 27, 2014 3:35 pm
Location: California

Re: $CHALK - function for colorizing text output using ANSI codes

Wed Jul 19, 2023 8:15 am

Could you clarify how to add it to RouterOS so that $CHALK can be used everywhere without copying it all over the place?
 
User avatar
Amm0
Forum Guru
Forum Guru
Topic Author
Posts: 4611
Joined: Sun May 01, 2016 7:12 pm
Location: California
Contact:

Re: $CHALK - function for colorizing text output using ANSI codes

Wed Jul 19, 2023 8:34 am

That's a philosophical question. Several ways actually, same as scripts.

Likely easiest is to use winbox to create a new System>Schedule...using "Start Time" as "startup" and interval 00:00:00...then cut-and-paste the $CHALK code above as the script. After reboot, the $CHALK should be loaded in terminal.

The alternative approach to loading function is save them as a file on the router, then just using ":import /flash/chalk.rsc"* to load only when needed in a script. (*assuming you save the code above to a file called chalk.rsc). You can also do same using /system/script to add a new script with same $CHALK code, then use "/system/script/run chalk" to load it say at the start of another script that used $CHALK.

Regardless of how you load it, in all case If you use it another function....you need to declare it using just ":global CHALK" e.g.
{
    :local myprint do={
       :global CHALK
       :put "$[$CHALK yellow]Hello $[$CHALK yellow inverse=yes]Kentzo$[$CHALK reset]"
   }
   
   $myprint
}

If you use the debug=yes in the command you just DIRECTLY use the generated ANSI code in your own strings. See description about debug=yes in original post above. So you load it only to generate the codes, so it does not be load if you just cut-and-pasted the ANSI from it ;)
 
User avatar
Kentzo
Long time Member
Long time Member
Posts: 653
Joined: Mon Jan 27, 2014 3:35 pm
Location: California

Re: $CHALK - function for colorizing text output using ANSI codes

Wed Jul 19, 2023 8:53 am

Interesting, didn't know about :import.
Regardless of how you load it, in all case If you use it another function....you need to declare it using just ":global CHALK" e.g.
Don't see this behavior being documented. Is it some sort of common knowledge that "imported" functions need to be re-declared?
 
User avatar
Amm0
Forum Guru
Forum Guru
Topic Author
Posts: 4611
Joined: Sun May 01, 2016 7:12 pm
Location: California
Contact:

Re: $CHALK - function for colorizing text output using ANSI codes

Wed Jul 19, 2023 9:00 am

Don't see this behavior being documented. Is it some sort of common knowledge that "imported" functions need to be re-declared?
Yeah you have to declare a global variable (function) inside another function. The "tips-and-tricks" linked from the script help page has some good tips, including this one about declaring use of a global variable in a different block/function:
https://wiki.mikrotik.com/wiki/Manual:S ... her_script

I mention the "rule" about declaring globals since the function not being loaded looks identical to it not be declared – the function gets skipped just the same in both case. ;)

Interesting, didn't know about :import.
:import just executes a file, it does not actually care if it's not "config code". The :import also has a verbose=yes option that will output the script as it's being interpreted... so useful to "debug" scripts, e.g. it will stop on a failing line – not quite a debugger but better than nothing.
 
User avatar
rextended
Forum Guru
Forum Guru
Posts: 12899
Joined: Tue Feb 25, 2014 12:49 pm
Location: Italy
Contact:

Re: $CHALK - function for colorizing text output using ANSI codes

Wed Jul 19, 2023 10:35 am

CHALK colorizing function code


:global CHALK do={
    \r\n Basic Syntax:\
    \r\n     \$CHALK <text-color> [<text>] [inverse=yes] [[bold=yes]|[dim=yes]]\
    \r\n \
    \r\n Alternatively, use set background (bg=) color, instead of inverse=yes:\
    \r\n     \$CHALK <text-color> [<text>] bg=<text-color> [[bold=yes]|[dim=yes]]\
If I can have my say, without offending anyone,
using this function, being called often, it would be better to simplify the syntax of the parameters...

Example:
from
:put "$[$CHALK yellow]Hello $[$CHALK yellow inverse=yes]Kentzo$[$CHALK reset]"
to
:put "$[$CS ye]Hello $[$CS = ye]Kentzo$[$CS]"

and the syntax from:
$CHALK <text-color> [<text>] [[inverse=yes] | [bg=<text-color>]] [[bold=yes] | [dim=yes]]

to
$CS <foreground-color> [<backgound-color>]
if no parameters are specified is implicit reset

foreground-color must be everytime specified, except is wanted reset
for keep the default foreground and change only background, put one = as parameter value.
color for foreground can be a named color, and since are present only 11 "colors", is useless write it completely, just use 2 letter abbreviation.
bk = black; bu=blue; rd=red; vt=violet; gn=green; tq=turquoise; ye=yellow; wh=white; gy=gr(a|e)y; ns=no-style; rs=reset (pk=pink; bn=brown; og=orange)
if a + is present, like +re mean the brigth variant,
if a - is present, like -re mean the dim variant,
if a ! is present, like !+re mean the bold variant with bright-red.
Can be specified also simply "+", "-" or "!" and is used the default color.

The same is valid for background color, except bold, which does not exist in the background.

What do not math on parameter, is simply ignored.
If a one or more +, - or ! are used, is considered only one time.
If some conflictual values are used, like + and - at the same time, the effect is neutralized inside the function
(Example: "if + is present set bright = bright + 1; if - is present set bright = bright - 1", so at the end bright is 0 and no color variant are used)
If one or more color are defined, are used only the first.
Last edited by rextended on Wed Jul 19, 2023 7:03 pm, edited 5 times in total.
 
User avatar
Amm0
Forum Guru
Forum Guru
Topic Author
Posts: 4611
Joined: Sun May 01, 2016 7:12 pm
Location: California
Contact:

Re: $CHALK - function for colorizing text output using ANSI codes

Wed Jul 19, 2023 4:21 pm

If I can have my say, without offending anyone,
Criticism is how things improve. :)

I only disagree with making the background a "positional parameter". It's actually not all the useful since inverse=yes will set the background as the color, the foreground is something reasonable. So kinda why it's an optional, but short, "bg=" for background color since in most cases "inverse=yes" be a better plan.

using this function, being called often, it would be better to simplify the syntax of the parameters...
I ain't picky on names ... initially concerned with the logic working right ;). But anyone can rename the global when scheduling/sys script/import... Now, I added TODO that help strings should use $0 instead a static CHALK so they'd refer the assigned variable name.

I think using it just to generate the code once is likely best plan – cut-and-paste the real ANSI code into a string but use CHALK (or CS) to help build them initially... e.g. the debug=yes option - which might be better named too.

color for foreground can be a named color, and since are present only 11 "colors", is useless write it completely, just use 2 letter abbreviation.
bk = black; bl=blue; re=red; ma=magenta; gn=green; cy=cyan; ye=yellow; wh=white; gy=gr(a|e)y; ns=no-style; rs=reset
if a + is present, like +re mean the brigth variant,
if a - is present, like -re mean the dim variant,
if a ! is present, like !+re mean the bold variant with bright-red.
Can be specified also simply "+", "-" or "!" and is used the default color.

I liked the + - ! modifiers – that's a good one – including the logic that they cancel each other out. Perhaps "!" be logical inverse=yes... Perhaps * for bold since that what markdown/etc use. Let me mull on this one.

On the short color names... As you can see, I just do a lookup in array by index since it simple... But similar to the CS name, changing color name key in the array to the shorter ones change black to bk be an easy "user mod". Or, just adding the "short colors" as additional values be another hack (I do this with gray, which is bright-black).

Tricky balance on how much command parsing to do... I thought about do some partial matching already so "ye","yel","yell" all meaning "yellow", like RouterOS CLI does – but just more code when the array can be adjusted to use whatever desired names to ANSI code mapping.
 
User avatar
rextended
Forum Guru
Forum Guru
Posts: 12899
Joined: Tue Feb 25, 2014 12:49 pm
Location: Italy
Contact:

Re: $CHALK - function for colorizing text output using ANSI codes

Wed Jul 19, 2023 5:13 pm

About "!", the first thing is to invert actual foreground and background colour, but for do that, you must keep track on used colors for foreground and for background.
Since "bold" put the text on... bold for have attention... ! is more mnemonical for me, at that moment, but thinking about *, probably is better *
and on future "/" remove bold, keeping same colors.

About short color names, are "studied" for not be mismatched.
No matter how any time or how many mix you do, each single code is unique.
Instead, for example "BL" can be... BLack? or BLue? // "GR"? is GReen??, GRay? (US) GRey? (UK) // "RE"? is REd? is gREen?
Using BK / RD / BU / GN / GY can't be confused...
Last edited by rextended on Wed Jul 19, 2023 6:58 pm, edited 2 times in total.
 
User avatar
Amm0
Forum Guru
Forum Guru
Topic Author
Posts: 4611
Joined: Sun May 01, 2016 7:12 pm
Location: California
Contact:

Re: $CHALK - function for colorizing text output using ANSI codes

Wed Jul 19, 2023 6:17 pm

Let me mull here. I actually have real work to do.

Good point, there is a standard for 2 letter color codes, IEC 60062:2016: https://en.wikipedia.org/wiki/Electronic_color_code
But yeah I didn't support partial matches since it wasn't all that useful ;).

There is ANSI encoding for RGB, so some color name to RGB might be fun. I'll probably add a different/seperate function to lookup more "advanced" color names, likely using the X11 color spec. e.g. you want "cornflower blue", no problem ;)