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:
- Restore terminal state (show cursor, etc.)
- Reset SIGTSTP back to SIG_DFL
- raise(SIGTSTP), now the OS default handler catches it, so the process actually suspends properly (no recursion)
- Execution resumes right after that raise when the user runs fg
Then in the SIGCONT handler:
- Re-apply your terminal state (re-hide cursor, etc.)
- 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).