Learn to build cli apps in Odin. Read best practices. Ask away if you need any help with your app.
Get the length of a string, excluding ansi codes:
count_chars :: proc(s: string) -> int {
result := 0
inside_ansi := false
for i in s {
if i == '\x1b' {
inside_ansi = true
}
if inside_ansi {
if i == 'm' {
inside_ansi = false
}
} else {
result += 1
}
}
return result
}
When it comes to measuring string width in the terminal you’ll also want to note that some characters are more than 1 column wide. I’ve borrowed from string-width/index.js at main · sindresorhus/string-width · GitHub before for measuring string width in CLIs. package unicode - pkg.odin-lang.org will come in very handy. For emojis I’ve used GitHub - mathiasbynens/emoji-regex: A regular expression to match all Emoji-only symbols as per the Unicode Standard. as a reference.
I’ve also found ANSI Escape Codes · GitHub to be a helpful reference at times.
I also have this note
Double-underline per ECMA-48, but instead disables bold intensity on several terminals, including in the Linux kernel’s console before version 4.17.
in my Elm ANSI package because it’s something I missed in that GitHub gist of ANSI Escape Codes. It’s a small note at the bottom of the Colors / Graphics Mode section.
@Feoramund wrote code that determines string width for terminal applications. See core:text/table for how it does this for tabular displays of unicode strings.
That’s excellent! Not sure I would have thought to look there, but still helpful to know it’s available.
Saw this while poking around to read about ANSI. It appeared the above comments about text length with the ANSI codes is related to formatting output after applying the ANSI. If not, you may ignore the following.
Another approach I found useful is to wrap the color codes around the format string and not the text. This way you don’t have to care about the text length with ANSI, and just format your output as normal based on the true text length.
Here’s an example of how I use this approach using Odin’s built in ANSI package
Edit: Accidentally named the below functions as 8bit when actually they should be 3bit. Example has been updated.
package scratch
import "core:fmt"
import "core:terminal"
import ansi "core:terminal/ansi"
// message types
Message_Type :: enum {
LABEL,
INFO,
STATUS,
OUTPUT,
ERROR,
}
printf_c3 :: proc(type: Message_Type, format: string, args: ..any) {
print_color_3bit(false, type, format, ..args)
}
printfln_c3 :: proc(type: Message_Type, format: string, args: ..any) {
print_color_3bit(true, type, format, ..args)
}
// wrap format string and pass to printf
print_color_3bit :: proc(newline: bool, type: Message_Type, format: string, args: ..any) {
using ansi
using fmt
color: string
switch type {
case .LABEL: color = FG_MAGENTA
case .INFO: color = FG_YELLOW
case .STATUS: color = FG_GREEN
case .OUTPUT: color = FG_BLUE
case .ERROR: color = FG_RED
}
if !terminal.color_enabled {
printf(format, ..args)
}
else {
pformat := tprintf("%s%s%s%s%s", CSI + FG_COLOR + ";", color, SGR, format, CSI + RESET + SGR)
printf(pformat, ..args)
}
if newline { println() }
}
// usage
main :: proc() {
printfln_c3(.INFO, "%-8s%s", "Hellope", "World!")
printf_c3(.LABEL, "%-8s", "Hello")
printfln_c3(.STATUS, "%s", "World!")
}
In a Windows only file, I use this proc to ensure UTF output in the Windows Terminal for abusing Unicode symbols
.
import win "core:sys/windows"
@(init, private="file")
init :: proc "contextless" () {
when ODIN_OS == .Windows {
win.SetConsoleOutputCP(win.CODEPAGE.UTF8)
}
}
For colored out, I usually leave that to static elements so will define a bunch of constants. E.g.
import "core:terminal/ansi"
PIPELINE :: ansi.CSI + ansi.FG_BLUE + ansi.SGR + "∴ pipeline:" + ansi.CSI + ansi.RESET + ansi.SGR
ASSETS :: ansi.CSI + ansi.FG_MAGENTA + ansi.SGR + "⇌ assets:" + ansi.CSI + ansi.RESET + ansi.SGR
RUNTIME :: ansi.CSI + ansi.FG_GREEN + ansi.SGR + "⨠ runtime:" + ansi.CSI + ansi.RESET + ansi.SGR
RENDERER :: ansi.CSI + ansi.FG_CYAN + ansi.SGR + "▦ renderer:" + ansi.CSI + ansi.RESET + ansi.SGR
These then get formatted into print/log statements as needed. For example, I don’t color the error message red, just prepend an error label.
