Simple TUI wraper for odin

I couldn’t find any stable and still supported TUI project for Odin. I decided to write a simple wrapper for ncurses (a well-known, old, stable library written in C).

I managed to port most of the library, so If anyone is interested in TUI, it might be useful:

The plan is to add support for macros of this library (they exist in the C precompiler so you can’t just link them) and finish the porting, then create wrappers so that you don’t have to manually handle C types.

As an example I make a simple file explorer

3 Likes

Hi, I am using termcl, started using it before the author announced it would not maintain it anymore :frowning:.

I am writing a relatively terminal file manager with it and it does work pretty well, albeit it’s lacking some features (handling terminal resize) and maybe has some bugs. The docs are basically absent but the source code is simple enough.

We could do something like fork it and do some maintenance. Although, I understand that ncurses is way more battle tested.

From what I can see, the library is small.

michal@debian:~/Pobrane/TermCL-main$ cloc .
      28 text files.
      26 unique files.                              
       3 files ignored.

github.com/AlDanial/cloc v 2.04  T=0.02 s (1548.3 files/s, 252069.1 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Odin                            25            588            338           3156
Markdown                         1             34              5            112
-------------------------------------------------------------------------------
SUM:                            26            622            343           3268
-------------------------------------------------------------------------------
michal@debian:~/Pobrane/TermCL-main$ 

On the other hand, ncurses is huge:

michal@debian:~/Pobrane/ncurses-6.6$ cloc .
    1251 text files.
     903 unique files.                                          
     348 files ignored.

github.com/AlDanial/cloc v 2.04  T=0.99 s (909.1 files/s, 352090.8 lines/s)
---------------------------------------------------------------------------------------
Language                             files          blank        comment           code
---------------------------------------------------------------------------------------
C                                      372          13236          24922          89770
Bourne Shell                            36           9971           8558          68781
HTML                                   243          10637           4447          48647
m4                                      20           1995           4012          22584
Ada                                    137           1521           6522           9968
C/C++ Header                            48           2085           3209           8789
C++                                      9            366            420           2016
Perl                                     2            168            121           1938
awk                                     13             58            505           1742
make                                     8            234            136            878
Windows Module Definition                4              0              0            649
DOS Batch                                4             37              0            438
Text                                     6             18              0            222
sed                                      1              0             75             43
---------------------------------------------------------------------------------------
SUM:                                   903          40326          52927         256465
---------------------------------------------------------------------------------------
michal@debian:~/Pobrane/ncurses-6.6$ 

Writing a proper interface between Odin and ncurses would amount to roughly 6,000 lines of solid code (rather than just mapping everything to [^]c.somethink and c.int). From a practical standpoint, it is simpler to integrate with an existing solution. Because it is better standardized.

But I’ve always wanted to get involved with something whit TUI. As far as I’m concerned, we could start something from scratch if you’re interested.

1 Like

From what I can see from my usage, termcl is pretty basic but usable.

I even think some parts of it could be dropped (there’s a widget system with 5 widgets that I haven’t bothered trying out). And there’s a SDL based backend which I am not sure what it would be useful for.

What I notice is missing for me from this library is:

  • No support for handling terminal resizes (need to catch some signal). It’s not hard to implement but I guess it’s needed for most terminal apps.

  • I am not the biggest fan of the api of setting styles. It looks like this:

    termcl.set_color_style(window, fg, bg)
    termcl.set_text_style(window, {.Bold})
    termcl.reset_styles()
    

    The main problem is that I want to change the fg color for part of the line, and I end up having to reset everything and reapply only the part I want. Maybe This pseudo-code explains it better:

    termcl.set_color_style(window, fg, nil)
    termcl.set_text_style(window, {.Bold})
    termcl.write(window, "my colored bold text")
    termcl.reset_styles()
    termcl.set_color_style(window, fg, nil)
    termcl.write(window, "my colored but not bold text")
    

    It would be nicer if the 3 styles were completely separate and could be reset on their own, including removing bold but leaving underline or similar. This would simplify setting styles a lot:

    termcl.set_color_style(window, fg, nil)
    termcl.set_text_style(window, {.Bold})
    termcl.write(window, "my colored bold text")
    termcl.remove_text_style(window, {.Bold})
    termcl.write(window, "my colored but not bold text")
    

    If your code become more complex with conditionals, this starts making a difference in readability IMHO.

  • There is no way to convert a key back to the string it was read from. This is sometimes useful, for example I have a keybinding config that I check against events and I want to show in a help page. Basically the reverse mapping that what the lib does on parsing.

  • I wish there would be an helper to manage scrolling though lists or pages longer than the screen.

  • Cherry on the cake would be a sort of input textbox. Not hard to write from scratch tough.

I think this these additions and removals, it would be a great little lib for doing terminal CLIs.

Oh, and maybe some minimal docs on how to use it :smiley: .

Unsure why ncurses is so large, but I guess it’s way more battle tested for all edge cases…

Every terminal is different. Some support GPU acceleration, some have their own dialect, and some only support 256 colors.

Due to a lack of consensus among implementations, a universal terminal database (Terminfo, usually “/usr/share/terminfo/”) was introduced.

Ncurses is a kind of API for this database (the only library supporting it comes from them and is part of the project).

I’ve taken a rough look at termcl, and it uses direct system calls to determine terminal information, which, in my opinion, is an architectural flaw: problematic support for multiple environments, especially since a solution already exists that handles practically all cases, and the system call causes a trap instruction that causes a context switch to the system kernel, which slows down the program.

The size of ncurses is due to its multi-functionality. During porting, I noticed that each function has ± 6 different implementations and several macros e.g.

  • printw - displays text
  • mvprintw - goes to (x, y) and prints this,
  • mvwprintw - goes to x, y inside the window object and prints text there,
  • mv_printw - the same but with _

and so on (the header has over 2000 lines of declarations, functions, types and macros, of which you can divide it by 6 to get the “minimum”).

I have already managed to port most of the API along with translating it to native types for Odin, so from a practical point of view, ncurses is better in terms of practicality.

I have a feeling that if I were to touch the TermCL code, I would be faced with a similar fun as migrating SQL Server 2005 to SQL Server 2021 along with a twenty-year-old software update in accounting production without any documentation about changes have in the database architecture or the program it self.

1 Like

Hi, I’m the original author of TermCL. I just wanted to clarify some things (see the replies).

TermCL originally started as an experiment on me trying to implement a lightweight ncurses like library after having written ncurses bindings for my then text editor I was attempting to write.

I started writing documentation on TermCL · RaphGL/TermCL Wiki · GitHub
but since I never had much feedback I didn’t really make adjustments or add anything new.

The widgets library is broken, I changed my plans to write a imgui style API for the widgets but didn’t get to implement it. You should scrap it if you ever forked. The kitty_keyboard implementation is incomplete and so it’s useless in its present state.


The SDL3 backend is there so that you can run a TUI without having a terminal. It was originally because I wanted to have a GUI and a TUI for my editor the same way emacs does. Also the backend API allows you to add support for weird platforms ncurses supports and I don’t.


I think the code is pretty easy to understand and the function comments tell u what they do. But something that I think is missing is telling people that having a window height or width of 0 means that it is allowed to stretch to the screen’s size.

Also the library prefers sending whole frames instead of only the chunks that changed because calling escape codes that change terminal state is slow, if you just know the size of the terminal buffer you can infer when the cursor is gonna wrap around, so I use that fact to make rendering faster. It might sound counter intuitive but sending a whole frame when many things change is actually much faster because moving the cursor around is too expensive and dumping frames has no moves. For the common case performance wouldn’t matter anyway, so you wouldn’t feel any sluggishness for sending more info than needed.

One downside to this is that I need to keep a window buffer allocated in memory and each window requires it’s own allocation. But doing this I went from 26 FPS to 100+ FPS on my stress test. Another is if your terminal is bad (yes!) you might get flickers if you don’t limit your frames.


Something that is incomplete in the library is the signal_*.odin files, they’re supposed to handle job control on linux (windows doesn’t have them so they’re pretty much complete ig), if you do ctrl + z and then fg the program has to save its state and restore the previous one, then when it’s waken up again it has to restore its own state, from what I remember, the restoring ur own state is a bit finicky rn.


I hope i answered some questions. feel free to fork or use my work as reference material for your own stuff!

No support for handling terminal resizes (need to catch some signal). It’s not hard to implement but I guess it’s needed for most terminal apps.

You don’t need this, I already retrieve the size before every draw. You’ll likely be running on a loop so you can easily compare previous and current sizes of the terminal to handle resizes.

There is no way to convert a key back to the string it was read from. This is sometimes useful, for example I have a keybinding config that I check against events and I want to show in a help page. Basically the reverse mapping that what the lib does on parsing.

I didn’t have this use cases while using/testing the library so it just slipped, but most of my use for something like this was using read_raw and then calling the parse_mouse_input and parse_keyboard_input myself instead.

Unsure why ncurses is so large, but I guess it’s way more battle tested for all edge cases…

Ncurses is old and wants to support every terminal under the sun, it queries a database for capabilities and escape code support and tries to have all these weird terminals behaving the same. TermCL completely rejects old terminals and only supports ANSI escape codes. I went out of my way to only test on terminals people actually use on windows, linux and macos. I don’t really see a reason to support old standards when 90% of people will never interact with them anymore and it makes the library way way simpler

I’ve taken a rough look at termcl, and it uses direct system calls to determine terminal information, which, in my opinion, is an architectural flaw

TermCL does no querying, it just starts from an assumption: “modern terminals only that support ANSI escape codes”. I went out of my way to pick escape codes that I know work in the widest possible amount of terminals people use (kitty, konsole, gnome terminal, windows terminal, wezterm, ghostty, alacritty).

: problematic support for multiple environments, especially since a solution already exists that handles practically all cases, and the system call causes a trap instruction that causes a context switch to the system kernel, which slows down the program.

The libraries underneath will also do the same system calls. You inherently need to talk to the kernel to be able to change io processing by terminals. Also not every syscall requires expensive context switching, I’ve tested my code against tview (the most popular tui lib in go) and actively ran its benchmark and I consistently beat it in the stress test.

I have already managed to port most of the API along with translating it to native types for Odin, so from a practical point of view, ncurses is better in terms of practicality.

Depends on what you want, if you want maximal portability (even to ancient terminals), ncurses will always beat termcl and most other libraries. it’s not even a contest. If you just want something to work on modern terminals (terminals that support ANSI escape codes) and don’t want a big dependency then TermCL would’ve been a good choice maybe.

I have a feeling that if I were to touch the TermCL code, I would be faced with a similar fun as migrating SQL Server 2005 to SQL Server 2021 along with a twenty-year-old software update in accounting production without any documentation about changes have in the database architecture or the program it self.

eh? I don’t like that lol. I write code as dumb as it can be, my code is very procedural and C style. I hate “enterprise” code

I only looked through the code a little, so I roughly guessed what a given function does based on its name and imports.

The database migration example represents “lack of 100% support.”

Microsoft only has a certain group of databases that can be migrated between each other (e.g., you have 2005 → 2008 coverage, but 2005 → 2021 only has 80% coverage, and 2008 → 2021 has 100% coverage (or something like that, I did it a long time ago)).

Which forced me to “set up a 2008 server, migrate 2005 → 2008, then export 2008, overwrite the 2021 database with 2008 data, and fix all incompatibilities” - that’s why I prefer a simpler API but with a guarantee that I can update the program after 20 years.

I plan to go back to C and Scala because Odin has a strange proportion of high-level and low-level concepts for me (I feel like I’m writing C++ code), so the conclusion is “Nothing will ever happen”

Sorry for reducing the topic to “the library is not in line with my beliefs, so it bad”

1 Like

Hi @RaphGL!

Thanks for your comments, really appreciate you made this library.

No support for handling terminal resizes (need to catch some signal). It’s not hard to implement but I guess it’s needed for most terminal apps.

You don’t need this, I already retrieve the size before every draw. You’ll likely be running on a loop so you can easily compare previous and current sizes of the terminal to handle resizes.

In my case, I do not use a mainloop that continuously render frames, rather I wait blocking on events and redraw if there is some user input. I do not see why I would continuously render when stuff can only change based on user input.

Then I need to know when the terminal resized so I can trigger a re-render. I implemented a signal in 10 lines to do this, but it could be provided as a native event for even more convenience.

It gets actually a little more complex than that, because there might be some internal events (e.g. a watched file changes) which also trigger, but I don’t continuously re-render frames, rather poll events and re-render when needed.

I didn’t have this use cases while using/testing the library so it just slipped, but most of my use for something like this was using read_raw and then calling the parse_mouse_input and parse_keyboard_input myself instead.

I am doing this, but I actually need to go from termcl.Key to the character it corresponds to (if any). Like termcl.A would become a. Sounds like a matter of reimplementing the big switch in the reverse direction.

Unsure why ncurses is so large, but I guess it’s way more battle tested for all edge cases…

Ncurses is old and wants to support every terminal under the sun, it queries a database for capabilities and escape code support and tries to have all these weird terminals behaving the same. TermCL completely rejects old terminals and only supports ANSI escape codes. I went out of my way to only test on terminals people actually use on windows, linux and macos. I don’t really see a reason to support old standards when 90% of people will never interact with them anymore and it makes the library way way simpler

I am actually totally fine for this. I do not care about supporting 80s terminal emulators :smiley:. One issue I have had however, is that I can’t get it to run in the intellij command run window, even with emulate terminal set to true. Something causes a SIGILL. But that’s just a matter of convenience for me.

The SDL3 backend is there so that you can run a TUI without having a terminal. It was originally because I wanted to have a GUI and a TUI for my editor the same way emacs does. Also the backend API allows you to add support for weird platforms ncurses supports and I don’t.

That’s actually a neat feature (haven’t tried hard enough to make it work). However, I think if I use termcl what I want is simply a text-based lib in 99% of cases. There might be some space for a text-based lib but rendered graphically, just as a way to make text-heavy GUI apps in a very simple manner, but maybe belong to another project.

The source code is very nice and easy to read, so it was not a problem to use the library, however a bit longer page of docs showcasing a couple features would help to quickstart.

Also, I do not mind writing my own widgets, however I think a scroll primitive (like a window that can scroll and maybe would make application code a lot simpler in most cases.

Another one could be a text input widget supporting proper text movement (e.g. home/end, ctrl+arrow, ctrl+delete or backspace), but on that one I am less sure as it would need to handle events somehow, and it gets complicated.

Currently, I am a bit time-starved but maybe in the future if I have some time I might check these issues more in depth.