Problem
Not long ago, I started to notice that many console utilities can output colored text. I was curious if I could also add colors to the output of my console version of Launcher.

The goal was to write an algorithm that can apply different colors to the fragments of the text message. In this article, we will take a detailed look at how to write such an algorithm in my favorite language - AutoHotkey - how to optimize this algorithm, and what to consider when writing high-level code.
If you are not familiar with AutoHotkey syntax, don’t worry, I’ll guide you through it. After all, one of the goals of this article is to introduce you to this language.
Naive Solution
Types of Functions
Let’s start with the idea of an early algorithm. Our goal is to change the color of a message when it is output to the console (also referred to as the terminal, as I work in the Cmder emulator with PowerShell). To do this, there must first be some markers in the message itself that will indicate the color for each part of the text. For example, HTML tags can serve as markers: <color>text</color>. Then we need to find all such tags in the text, extract the color name, and apply it to the console.


Fortunately, Microsoft allows us to access the Windows OS and ask it to do something. For this purpose, Windows API exists (Windows Application Programming Interface, also known as WinApi or simply API functions). Each function has its own documentation on the Microsoft website. When the AutoHotkey interpreter starts, it loads dynamic-link libraries (DLL), that contains this functions. We can call this functions using the built-in language function DllCall. All built-in AutoHotkey functions like MsgBox or FileAppend call one or another WinApi function, but if you really want to, you can “go down to a lower level” and call them manually.
Don’t confuse: user functions are defined manually; built-in functions are provided by the interpreter; WinApi functions can only be called via DllCall.
To change the color in the console, there is a WinApi function called SetConsoleTextAttribute. It takes the console handle (its unique number/id) and a number that “represents” the color. This is not an RGB or HEX representation, but a special internal flag that we will not focus on.
For further output to the console, there is a language function called FileAppend, which can output a message to text or to the console (the special file name - CONOUT$).
In the earliest version of this algorithm, I tried recursive HTML color tags processing with subsequent SetConsoleTextAttribute() calls, followed by FileAppend() calls:
Print(text, color := 'white') { ; Dictionary/HashMap that "maps" color name with it's special code. ; "static" means "initialized only once at script startup" ; (improves performance a bit) static colors := Map( 'black', 0, 'blue', 1, 'green', 2, 'cyan', 3, 'red', 4, 'magenta', 5, 'yellow', 6, 'white', 7, 'gray', 8, ) ; Get default color code, it will be used later ; as default text color normalColor := colors.Get(color, 7) _Print(msg, _color := normalColor) { ; Closure: user-defined function inside other user-defined function ; Very useful for repeating tasks like console output ; Get console uniq id. ; "static" because console id doesn't changes, ; Therefore we can intialize this variable only once and re-use it static hConsole := GetOutputHandle() DllCall( 'SetConsoleTextAttribute', 'ptr', hConsole, 'uint', _color ) ; Append colorful text to the console FileAppend(msg, 'CONOUT$') } pos := 1 while (pos <= text.length) { ; Search for color tags starting from `pos`, ; store found result in match variable if (RegExMatch(text, 's)<(\w+)>(.*?)</\1>', &match, pos)) { ; Print normal text before the match normalText := text.Slice(pos, match.pos - pos) if (normalText) _Print(normalText) ; call closure above ; Handle nested tags ; Recursive call to this function with 2nd captured group as a message ; and 1st captured group as a color Print(match[2], match[1]) ; Move position forward pos := match.pos + match.len } else { ; Print remaining text _Print(text.Slice(pos)) break } } } Message(msg, icon := '', normalColor := 'white') { if IsConsole return Print(msg '`n', normalColor) msg := RegExReplace(msg, 's)<(\w+)>(.*?)</\1>', '$2') return MsgBox(msg, A_ScriptName, icon) }
RegExMatch is built-in function that allows to parse text faster without high-level language drawbacks.
As you can see, it looks pretty simple: find the color, apply the color, output to the console, repeat. This algorithm also handles nested tags: <yellow>my colorful<cyan>message</cyan></yellow>
Since the Print() function only processes and outputs the message, we need an additional Color() function that will add HTML tags to the text:
Color(str, regex, colorTag) { return RegExReplace( str, regex, Format('<{1}>$0</{1}>', colorTag) ) }
Another built-in function that searches for a piece of text and wraps it in tags. Minimalistic and convenient.
New String Methods
For convenience, we can declare additional methods for strings, similar to those provided in Python. However, in AutoHotkey, unlike Python, we can declare any strings methods we want, such as Color() method:
'My colorful message'.Color('message', 'cyan') ; or '-help, -h or -? displays help message'.Color('-\w+', 'green')
As you may have guessed, the Color() method will call our Color() function with the string as 1st argument. That’s it, 'My colorful message'.Color('message', 'cyan') will be equivalent to Color('My colorful message', 'message', 'cyan').
This construction declares a new Color() method for primitive of type String:
({}.DefineProp)(String.prototype, 'Color', {call: Color})
In the algorithm above, you may have noticed the Slice() method and the length property. In fact, these are also “added methods” that are wrappers around language functions. They make the code more readable.
Let’s see how we can use our algorithm. In codebase there is PrintHelp() function with the help messages: “usage” part contains tags but “synopsis” part is not. Below is a small part of it (since this messages are quite large):
PrintHelp() { usage := ( "<gray>Launch saved scripts and applications. Copyright (c) 2026 Rafaello https://github.com/JoyHak/Launcher</gray> Usage: launcher --param=script1<gray>[;script2;script3...]</gray> launcher --param=<yellow>@file</yellow> launcher -switch launcher <cyan>variable</cyan>=value" ) synopsis := ( "Parameters: --run run script(s) --close close script(s) --add add script(s) --remove remove script(s) --sep set separator between scripts" ) synopsis := synopsis .Color('[\-]+[\-\w]+', 'cyan') ; String concatenation: a . b Message(usage . synopsis)
The usage message already contains color tags, Color() is applied only to the synopsis message and colors the --switches.
It looks good. But what lies under the hood of these functions and how many operations do they perform on each call? The FileAppend() function repeatedly opens and closes access to the console. The SetConsoleTextAttribute function performs a number of operations that are unrelated to color changes: polling the console mode, checking the free output buffer, etc. Finally, there is a risk that recursion in Print() may fail with deeply nested tags and cause a stack overflow.
Execution Time
Let’s try to measure the execution time of the algorithm. To do this, we will use a wrapper around the WinApi QueryPerformanceCounter function:
Timer(_start := false) { static previous := 0, frequency := 0, current := DllCall("QueryPerformanceFrequency", "Int64*", &frequency) _result := !DllCall("QueryPerformanceCounter", "Int64*", ¤t) if _start { previous := current _result += current / frequency } else { _result += (current - previous) / frequency } return _result * 1000 ; seconds to milliseconds }
The usage is simple: Timer(1) starts a timer, and Timer(0) stops it and returns the time in milliseconds. Let’s measure the execution time of the large help message processing and write this time to the file:
PrintHelp() { ; ... Timer(1) synopsis := synopsis .Color('[\-]+[\-\w]+', 'cyan') ; switches examples := examples .Color('(mainDir|AhkDir)(?=\=)', 'cyan') ; variables names .Color('%[^%]+%', 'blue') ; variables values .Color('@(file|list\.ini)', 'yellow') ; list Message(usage . synopsis . examples) time := Timer(0) FileAppend( Format('Color tags, multiple SetConsoleTextAttribute() calls: {:.4f}ms`n', time), 'C:\Temp\launcher_bench.log' )
Since output to the terminal works for executable files only, we will first build our launcher.ahk into launcher.exe and save it in the v1 directory. After running &"C:\Temp\v1\launcher.exe" -help in the terminal we can see the 77.6859ms exec. time in the file.
Not bad, although we can see that text appears part by part. Can we optimize this code?
Efficient Solution
ANSI Codes
As I wrote above, the slowest part here is the combination of SetConsoleTextAttribute and FileAppend. It would be great to leave only one call to FileAppend. But that means we’ll have to preprocess the message manually, without calling SetConsoleTextAttribute.
I found out that SetConsoleTextAttribute adds special codes to the message. Although they are historically called ANSI codes, they are actually an extension of the current encoding (usually Windows-1252).
Furthermore, the function itself is quite limited, as the number of these codes (and encoding methods) is much larger. My Cmder emulator, like many other good terminal emulators, is capable of processing these codes and displaying 256+ colors. I’m not sure if Windows Terminal is capable of this.
Thus, our algorithm must be able to append ANSI codes to the message. For simplicity, we will use a single-byte character set. This codes looks like this: \e[0;31m - red, \e[0;34m - blue, etc. It also allows to encode text emphasis, like \e[1;31m - bold red or \e[0;41m - red background, but we’ll only look at the foreground colors
To color the message, it is enough to “wrap” the found text part with the specific ANSI code according to the found color tag: \e[1;31mmessage\e[0m where \e[0m is the “stop” code. The text will be rendered with default color after this code (white by default, but depends on the emulator settings).
In that case, let’s rewrite our algorithm so that it adds this codes. We’ll give each code a meaningful name, which we’ll expect as the color parameter in the input:
Color(text, color := 'white') { ; Dictionary/HashMap that "maps" color name with ANSI code. static colors := Map( 'black', 30, 'red', 31, 'orange', 33, 'magenta', 35, 'gray', 90, 'crimson', 91, 'green', 92, 'yellow', 93, 'blue', 94, 'purple', 95, 'cyan', 96, ) ; Default color for this part of the text normalColor := colors.Get(color, 37) static esc := Chr(27) ; ASCI escape character \e static end := esc '[0m' ; Stop color processing: \e[0m begin := esc '[0;' normalColor 'm' ; Start color processing, code like \e[0;37m clrText := '' ; result: colored message pos := 1 ; text beginning while (pos <= text.length) { ; Search for color tags starting from `pos`, ; store found result in match variable if (RegExMatch(text, 's)<(\w+)>(.*?)</\1>', &match, pos)) { ; Normal text before the match with default/input color ; (depends on recursion level) clrText .= begin . text.Slice(pos, match.pos - pos) . end ; Nested tags clrText .= Output(match[2], match[1]) ; Move position forward pos := match.pos + match.len } else { ; Remaining text clrText .= begin . text.Slice(pos) . end break } } return clrText } Print(msg, icon := '') { if IsConsole return FileAppend(msg '`n', 'CONOUT$') msg := RegExReplace(msg, 's)<(\w+)>(.*?)</\1>', '$2') return MsgBox(msg, A_ScriptName, icon) }
The code has become much simpler. Instead of two independent algorithms for text highlighting and parsing, we got one small algorithm in the Color() and a small algorithm in the Print(), which only outputs the finished message with one FileAppend() call.
After running &"C:\Temp\v2\launcher.exe" -help we’ll see 3.5338ms. 21.9836 times faster than naive solution! Nice example of why many functions calls are bad.
Solution Without Tags
Despite the convenient algorithm, we still have some drawbacks:
HTML tags are inconvenient to write by hand in some text editors like Notepad++.
HTML tags lengthen the message and slow down parsing.
Let’s try to reduce the number of characters in the help text. If you are familiar with Markdown, you know the text formatting symbols: # __ `
When rendering a .md document, they disappear to focus attention on the text using formatting rather than symbols.
Instead of HTML tags, we can create a similar syntax like #header#, magenta etc., which is easy to parse:
PrintHelp(*) { ; ... msg := usage . synopsis . examples Timer(1) msg := msg .Color('(@(file|list\.ini))', 'yellow') ; list .Color('(mainDir|AhkDir)(?=\=)', 'purple') ; variables names .Color('(%[^%]+%)', 'blue') ; variables values .Color('(\-+[\-\w]+)(?=[ =])', 'cyan') ; switches .Color('\*\*([^\*]+)\*\*', 'crimson') .Color('__([^_]+)__', 'magenta') .Color('(~)', 'gray') .Color('(``)', 'green') .Color('(#)', 'orange') .Print() time := Timer(0) FileAppend( Format('ANSI codes, multiple .Color() calls: {:.4f}ms`n', time), 'C:\Temp\launcher_bench.log' ) }
In this code we expect that it is possible to color the text inside the characters by passing the corresponding expression and the expected color. For example, for blue text surrounded by percent signs %, we pass pattern like (%[^%]+%).
To support these expectations, it is necessary to rewrite the Color():
Color(msg, regex, color) { ; Dictionary/HashMap that "maps" color name with ANSI code. static colors := Map( 'black', 30, 'red', 31, 'orange', 33, 'magenta', 35, 'gray', 90, 'crimson', 91, 'green', 92, 'yellow', 93, 'blue', 94, 'purple', 95, 'cyan', 96, ) static esc := Chr(27) ; ASCI escape character \e static end := esc '[0m' ; Stop color processing: \e[0m begin := esc '[0;' colors.Get(color, 37) 'm' ; Current text color code ; Parse the message pos := 1 len := msg.length clrMsg := '' ; result (colorful message) while (pos <= len) { if !RegExMatch(msg, regex, &match, pos) { ; Remaining text clrMsg .= msg.Slice(pos) break } ; Normal text before the match clrMsg .= msg.Slice(pos, match.pos - pos) ; Apply color code clrMsg .= begin . match[1] . end ; Move position forward pos := match.pos + match.len } return clrMsg } Print(msg, icon := '') { if IsConsole return FileAppend(msg '`n', 'CONOUT$') msg := RegExReplace(str, 'U)' esc '\[\d+(;\d+)?m') return MsgBox(msg, A_ScriptName, icon) } ; Declare additional String methods ({}.DefineProp)(String.prototype, 'Color', {call: Color}) ({}.DefineProp)(String.prototype, 'Print', {call: Print})
Now we iterate through each part of the text using a loop and with added Print() method we can create calls chain: "My colorful message".Color("message", "cyan").Print().
After running &"C:\Temp\v3\launcher.exe" -help we’ll see 2.5428 ms. Awesome! The algorithm has gone from recursive to iterative, and readability has improved slightly.
The source code of the text output algorithm is available on GitHub. And here you can read about some additional features of this algorithm.
Conclusion
The first solution is not always the best one. Speed matters if you’re writing a small AutoHotkey script. But if you’re working on full-fledged projects that handle complex tasks, it makes sense to take a step back and try to make the codebase more readable and the algorithms more efficient.
In this article, we’ve seen that AutoHotkey is not a primitive language for writing macros and remapping keys. It’s a complete programming language on which you can write algorithms and your own syntax sugar.
In the next article, we’ll try to further optimize this algorithm, explore nested ANSI colors, and look at some interesting features of regular expressions. Check out my GitHub if you want to see the AutoHotkey capabilities in action!