I have been working on a small TUI application (with ratatui, what else?) to check the spelling of my long-winded articles and fix the mistakes of my fat fingers.
It obviously would not catch everything, but it is a decent effort.
You can check it out here until I get to write something more substantial about it.
One thing I did want to do, however, is to spell check Arabic. As well known to anyone who tried, Arabic and terminals do not mix well. Different terminals support Arabic in different broken ways. Ghostty, for example, just does not bother doing right-to-left at all. Zed terminal, for the time being, renders each word "correctly", but the whole sentence flows left-to-right. The macOS Terminal.app, renders Arabic fine, and supports Bidirectional text fine. But ..
Here is what the TUI looks like when checking an English article:

Here is what it looks like when invoked on an Arabic article:

Terminal.app does the correct thing for blocks of text, but it has no concept of a TUI. It does not know that this is actually a table that has separate things.
However, since Terminal.app thankfully implements the Bidirectional Algorithm, you can signal to it, (without breaking anything else!) that the text should have no effect on its surroundings.

Voila!
The trick for that is a small group of Unicode control characters called Explicit Directional Isolates.1 What these characters do is that they tell the renderer, in this case Terminal.app, that the text inside them, has a direction that is isolated from its surroundings.
It does not actually matter if the inside text matches the outside text or not. The inside direction can even be inferred out of the internal text!
Anyway, without further ado, here is the utility function I made:2
// Bidi forcing characters
const LRI: &str = "\u{2066}";
const FSI: &str = "\u{2068}";
const PDI: &str = "\u{2069}";
fn isolate_str(i: impl Display) -> String {
("{LRI}{FSI}{i}{PDI}")
}
And it is used thus (in a few places).
if let Action::Replace(s) = a {
Cell::new(Line::from_iter([
Span::from(isolate_str(s)), // <--
Span::from(" "),
Span::from(isolate_str(c.word)) // <--
.style(Style::new().crossed_out().dim()),
]))
} else {
Cell::new(isolate_str(c.word)) // <--
},
And Bob's your uncle!
What this does is conceptually simple:
LRI tells the renderer everything after it is left-to-right.
FSI tells it that everything after it is an isolated direction from before, and to infer what it is from the "first strong" character.
PDI just pops the FSI out of the stack to go back to left-to-right.
In effect, the whole incantation just prevents the renderer from rearranging the whole line as if it is a proper bidirectional line.3
For terminals that do not implement the Bidi Algorithm, like Ghostty and whatever Zed uses, there are zero effects. They ignore these characters anyway.
So, if you are a TUI writer who is struggling with right-to-left input, rest knowing it is an easy fix on your end. If you're a user struggling with a TUI, just point the author to this page.
Maybe this could be even upstreamed to ratatui itself. That's for braver souls than me, however.
I am traveling to Syria tomorrow. Wish me luck and free Palestine.
-
Note these are relatively newer additions to Unicode, and the recommendation for all new texts. Directional Embeddings were found to behave too erratically, and Directional Overrides were found to have security implications and should now actually be ignored. ↩
-
I do dislike that it allocates unnecessarily. ↩
-
Although on further introspection, it seems one might only need the
LRIcharacter at the beginning of the line. ↩