ANSI Printing Library/Collection - afmt

Nice!

I’ve been working on a package for a while. I recently got a claude code sub and decided that would be the time to get it finished.

I gave afmt and termCL both shoutouts. I mostly built this set of packages for myself so I can rebuild a tool I wrote in python into odin and give my users the same experience.

Here is how I had Claude build the signal handling. It seems to work.

https://github.com/davised/odin-cli/blob/main/term

For the SIGTSTP/SIGCONT issue, the trick that worked for me is the reset-then-raise pattern:

In the SIGTSTP handler:

  1. Restore terminal state (show cursor, etc.)
  2. Reset SIGTSTP back to SIG_DFL
  3. raise(SIGTSTP), now the OS default handler catches it, so the process actually suspends properly (no recursion)
  4. Execution resumes right after that raise when the user runs fg

Then in the SIGCONT handler:

  1. Re-apply your terminal state (re-hide cursor, etc.)
  2. Reinstall your custom SIGTSTP handler for the next Ctrl+Z

You don’t need to reinstall SIGCONT from within SIGCONT, that’s probably what’s causing your loop.

The other thing that matters: use sigaction() instead of libc.signal(). With signal(), some platforms (musl libc) reset the handler to SIG_DFL after it fires once, so your handler silently disappears. sigaction() keeps it installed across deliveries. Odin has it in core:sys/posix.

Also worth noting, signal handlers should only do async-signal-safe operations (raw posix.write, atomics). Calling fmt or anything that allocates from inside a handler is UB so it’ll work most of the time but can deadlock if the signal arrives mid-allocation.

I have some example code that handle suspend and resume properly, so while I don’t understand the whole process (Claude helped), I did some downstream testing to confirm the solution does seem to work.

Edit - I just reread your code and I see you already pretty much do what I suggest. I think the problem is reinstalling SIGCONT from within the SIGCONT handler. That’s what causes the discard/restart behavior you’re seeing.

The fix is: install SIGCONT once and never touch it again. sigaction keeps handlers installed across deliveries, so there’s no reason to reinstall it. The only handler you need to reinstall from within SIGCONT is SIGTSTP (since you reset that one to SIG_DFL to let the process actually suspend).

2 Likes

Just a heads up that I’ve been rather busy lately so I haven’t had the time to look into it.
After the 6th I should have the time to look into it again.

I really appreciate both of you for taking the time to help me out with this!

I’ve gone and did the SIGCONT fix that was suggested, it seems to be working somewhat, but I still have some issues with the TUI not responding to input after putting it back in foreground, I also need to be able to restore some state. On windows this is done for you automatically apparently but on POSIX I need to keep track of mouse input, cursor visibility, etc.

I’ve pushed a release but I’ll have to go back and check why that is happening on POSIX.

Update
Just did a fresh install of latest Windows 25H2, and found that only 4bit was passing through afmt. I was relying on the terminal.color_depth set through core:terminal, but after looking into it deeper, it was simply arbitrarily setting Windows to 4bit. I’m pretty sure Windows supported 24bit since around 2016. In addition, some other conditions in core:terminal sets the bit depth inaccurately. In it’s defense, there would need to be extensive work done to add more robust checking. Some methods/terminals are inherently unreliable.

With that, I have removed the bit depth check in afmt for now. This should open it up to more terminals/environments. The drawback, is you will have to decide what depth you wish to support if planning to run a program in multiple environments.

Also, some new functions added. Most notably, set_utf8_terminal and reset_utf8_terminal for Windows, to allow for greater support of printing utf8 runes. On my fresh install, Windows defaulted to CODEPAGE IBM437, so some characters did not display from the examples in afmt until setting CODEPAGE UTF8. The latest version of Windows does have a setting for enabling UTF8 system-wide, but it is currently label as “Beta”.

New procedures (examples.odin also updated)

  • tset - returns ANSI format string without RESET at the end using context.temp_allocator
  • aset - returns ANSI format string without RESET at the end using provided allocator. Default is context.allocator.
  • RESET - is a constant definition of the ANSI reset sequence.
  • RST - alias of RESET
  • set_utf8_terminal() - Sets terminal CODEPAGE to UTF8. Only operates on Windows. It will do nothing on all other environments, so it is safe to not check ODIN_OS if you so wish.
  • reset_utf8_terminal() - Reset terminal CODEPAGE to original prior to using set_utf8_terminal. Only operates on Windows. It will do nothing on all other environments, so it is safe to not check ODIN_OS if you so wish.

Found here: afmt.

1 Like

I remember last year when I was first writing TermCL and I was checking how NCurses determines what colors are supported and they just use termcap/terminfo to know what are the capabilities of the terminal. You could go on and try a bunch of heuristics yourself to determine whether that’s true, but if memory serves me right there’s no actual guarantee, users can just lie to you.

So I just said fuck it and decided I will not support any non modern terminal. I just installed a bunch of modern terminals (Konsole, Alacritty, Kitty, Windows Terminal and so on) and wrote my library by testing what worked on all of them, if something didn’t work or was finicky I just removed support for them and moved on.

If for some reason users want to support the most amount of terminals anyway, they would just use the 8-bit color palette, if they want more they can use the true colors and expect it to work on any terminal worth using. (yes, if your terminal doesn’t support any of the stuff I support on TermCL I just assume you’re irrelevant :P)

Lol. That was my ultimate decision, but not quite so succinctly put.

1 Like

More updates. This one changes all the enum values to lowercase. The enum object names remain upper snake case. Also removed the fg and bg prefixes, since they are redundant and just adds more width to syntax per line. I started with the common practice of naming enum value names with upper snake case, but through usage and lessons learned, sometimes it’s better to break with standard practice for a more enjoyable experience. I figured since the string formatting method uses lowercase and does not use the fg and bg prefix on color names, it would be more consistent if all the structs did the same.

Affected enums:

Attribute

FGColor3
BGColor3

FGColor4
BGColor4

New constructor procedures

ansi3 :: proc(fg: FGColor3, bg: BGColor3, at: bit_set[Attribute] = nil) -> ANSI3
ansi4 :: proc(fg: FGColor4, bg: BGColor4, at: bit_set[Attribute] = nil) -> ANSI4
ansi8 :: proc(fg: Maybe(u8), bg: Maybe(u8), at: bit_set[Attribute] = nil) -> ANSI8
// the most useful of the bunch is ansi24, since it now allows to also use enums for color names.
ansi24 :: proc(fg: union{RGB, Color}, bg: union{RGB, Color}, at: bit_set[Attribute] = nil) -> ANSI24

Colors defined in colors.odin re-sorted into groups matching Extended_colors.
Updated color guide procedure and a new one.

// Chosen groups are printed as either bg or fg depending on bg parameter.
// Color of text is set to white or black depending on contrast_ratio

print_color_guide :: proc(groups: bit_set[Color_Group] = {.all}, bg := true)

// Chosen groups are printed as either bg or fg depending on bg parameter.
// Color of text is NOT changed depending on contrast_ratio
// Use test_color to test and view a second color with the chosen groups. It will be either bg or fg, the opposite of bg: bool, which is used for the chosen groups.

print_color_guide_ex :: proc(groups: bit_set[Color_Group] = {.all}, bg := true, test_color := Color.black)

All other projects that use afmt have been updated for this change as well:

  1. tracker
  2. aftv
  3. progressbar

As always, I’m open to feedback, requests, bug reports. I’m even fine if you tell me this was not the best approach or you dislike this library. If you have an idea for a better approach, please let me know.