Kakoune: Switch away from ncurses to terminfo-based input handling

Created on 4 Nov 2018  路  25Comments  路  Source: mawww/kakoune

This was brought up many times, and is indirectly implied by a few existing issues asking to distinguish certain keyboard mappings regardless of the taken approach. Contrary to all those issues, this ticket is proposing a specific approach: switching away from ncurses to terminfo-based input handling.

Input handling is a key aspect of code editor, I strongly believe this should be done directly using terminfo and not via some third-party wrapper libraries such as ncurses or libtickit. The current limitations of ncurses are simply embarrassing. Terminals evolve, if a new solution / xterm protocol extension gets adopted, we must be able to enable it quickly, not wait for months or years until it's adopted by a library of choice.

If this is implemented, this fixes issues like #2553, #2209, #1527, #1270, #997, #970 and #1012.

Most helpful comment

Once we get rid of ncurses, we just have to remove our dependency on the standard C++ library and we are done ! (kidding, or am I ?)

And then rewrite everything in Rust.

All 25 comments

I agree. Just to clarify, do you suggest we should also drop ncurses for rendering or should this be tackled in a separate task?

If possible, I suggest to tackle that separately, primarily to simplify the transition.

Abandoning ncurses' input-handling and using raw terminfo would make it easier to handle keys like <c-left> (which are listed in the terminfo database), but it wouldn't help with most of the issues you list:

  • #2553 is about mapping ASCII NUL; glancing at the code I think this is just because Kakoune says > 0 where it means >= 0.
  • #2209, #1527, #1270, #970 are about the fact that terminals use control-characters for some special keys. That's a fundamental truth of terminals, regardless of whether you use ncurses, terminfo, or write custom code yourself. #1012 (using libtickit) helps if you're using a terminal that implements libtickit's extended-input mode, which (as far as I know) no common terminals actually do.
  • For #997, I cannot fathom how <a-right> became bound to <a-f> (it's not a standard terminal binding at all) and the comments suggest it's a weird Mac thing. It would remain a weird Mac thing regardless of what terminal input library Kakoune uses.

Well somehow nvim is able to distinguish certain mappings that kakoune cannot (such as c-j and ret), so I'm sure it's possible. Furthermore, if we use terminfo-based input handling, it is very simple to support modern terminals - for example kitty supports something called full keyboard mode that allows to distinguish each and every key combination, and in order to support that the application needs to simply send out a single escape code that some terminals will respect and others will silently ignore. This is impossible with ncurses or libtickit.

I admit my collection of issues might not be perfect, I mostly wanted to showcase a scale of how often this topic is brought up.

Normally <ret> sends <c-m>, but because the line-ending convention on Unix is <c-j>, ncurses by default converts <c-m> to <c-j> so they're indistinguishable. ncurses also provides the nonl() function to disable that conversion and keep them separate, which Kakoune uses. However, Kakoune re-implements the conversion itself, I'm not sure why. That still leaves <ret> and <c-m> as indistinguishable, but I believe that's the case in (N)Vim too.

Kitty's "full keyboard mode" does not appear in its terminfo database, so terminfo support won't help with that.

Just to be clear, I'm not arguing that we shouldn't replace ncurses input handling with raw terminfo handling. I'm sure it would make the code cleaner, and a bit more flexible since some extended terminfo keys (like <c-right>) are a pain to use via the ncurses API. But unlike output formatting (where terminfo is much more flexible than ncurses, and raw codes much more flexible again), terminfo input handling isn't that much of an advantage.

Thanks for explaining, interesting. In summary, if I understood everything correctly, by switching to raw handling we will be limited not by a library of choice, but by terminals themselves: for old terminals there will be little advantage, but for modern terminals we will be able to distinguish more, or even all keyboard shortcuts. As a user of a modern terminal I consider this a big advantage 馃槃

I'll open an issue in kitty so by the time this ticket is addressed, kitty's terminfo database will be ready for full keyboard mode 馃槈

Thanks for reaching out to kitty @maximbaz Plainly supporting modern terminals will also mean having nice support for UI features like curly underlines.

With the goal of fully removing ncurses in the long run (kakoune will be dependency-free), I think it would make sense to approach the development by copying ncurses_ui.hh and ncurses_ui.cc to new files term_ui.hh and term_ui.cc.
This way we could iterate on this new code without risk of touching the existing one, because I expect we may inadvertently break things in the beginning on a few edge cases. Beta users could then start kakoune with kak -ui term to see if it works for them.
It is a good opportunity to gradually include pieces, starting by the keyboard input and then the rendering part, and gather feedback along the way for a broad audience.

Just want to mention, this is another perfect example why removing dependency is beneficial: I reported an issue to kitty and _2 hours later_ it was fixed, new terminfo file is already available for download. Imagine how many _years_ it would take for ncurses to update kitty's terminfo before ncurses will be able to detect that kitty supports this new term capability (I assume this is how it works, this is why ncurses bundles various terminfo files in its distribution).

With the goal of fully removing ncurses in the long run (kakoune will be dependency-free), I think it would make sense to approach the development by copying ncurses_ui.hh and ncurses_ui.cc to new files term_ui.hh and term_ui.cc.

I think I would prefer to avoid duplicating all that code. If we want to experiment while keeping a fallback (not really the traditional Kakoune way, which has often been to just break it), adding a ncurses_input_stack=ncurses|terminfo ui_option would do the trick.

Once we get rid of ncurses, we just have to remove our dependency on the standard C++ library and we are done ! (kidding, or am I ?)

Once we get rid of ncurses, we just have to remove our dependency on the standard C++ library and we are done ! (kidding, or am I ?)

And then rewrite everything in Rust.

I think I would prefer to avoid duplicating all that code. If we want to experiment while keeping a fallback (not really the traditional Kakoune way, which has often been to just break it), adding a ncurses_input_stack=ncurses|terminfo ui_option would do the trick.

Since there's no real point to keep ncurses for that job in the long run, why not just create a branch that removes ncurses? Once it's ready/tested then we can just merge the branch into master.

Let's try to recap the current state of the art to see if we're on the same page.

As far as I understand, both libtermkey and libtickit libraries share the same author. libtermkey is now deprecated since it handles only input and libtickit is a superset which also does the drawing part.

http://www.leonerd.org.uk/code/libtermkey/
http://www.leonerd.org.uk/code/libtickit/

libtermkey is used by vis and neovim

The idea behind these libs is explained in http://www.leonerd.org.uk/hacks/fixterms/

But then, 1 year ago, the authors of libtermkey, neovim and kitty exchanged a few words on this topic: https://github.com/kovidgoyal/kitty/issues/94#issuecomment-341995869
They discussed the libtermkey solution vs the one implemented by kitty: https://sw.kovidgoyal.net/kitty/protocol-extensions.html#keyboard-handling

From what I understand, the kitty approach seem more complete than libtermkey.
The concept of lossless keyboard input was also a strong argument during the (now suspended) development of notty: https://github.com/withoutboats/notty

Don't hesitate to correct me or add other pieces of info, so we can get a broader image of the current solutions.

One small question: if Kakoune will ever have official GUI (what? I think that the project can benefit a lot if it will have both TUI and GUI, like Emacs. It also adds lots of features beyond fancy window), would it be as restricted in terms of keyboard support as it's terminal version?

E.g. in Emacs some keys are bound to corresponding terminal ones by default, like Ctrl+m is mapped to Return, despite the fact that GUI Emacs can distinguish it from return key, but that brings consistent behaviour between GUI and TUI.

Since there is no official GUI yet, nobody can definitively say what it would or would not be able to do. However, I've worked on an unofficial GUI, and (among other things) it can definitely distinguish <c-m>, <c-j> and <ret>. On the other hand, Kakoune only allows the shift modifier to be used with ASCII letters and special keys, not other printable characters, and that restriction does remain in my unofficial GUI.

Since there's no real point to keep ncurses for that job in the long run, why not just create a branch that removes ncurses? Once it's ready/tested then we can just merge the branch into master.

We can do the work progressively on master, fix whats detected as broken, and once ncurses_ui does not use ncurses anymore, rename it.

One small question: if Kakoune will ever have official GUI (what? I think that the project can benefit a lot if it will have both TUI and GUI, like Emacs. It also adds lots of features beyond fancy window), would it be as restricted in terms of keyboard support as it's terminal version?

Its unlikely that Kakoune will come bundled with an official GUI, as this would mean blessing some kind of UI toolkit and dependency. UIs can be implemented through the json-rpc ui protocol, and I designed the UI system with non-terminal UIs in mind (although its not very well tested, so as @Screwtapello said, their might be some arbitrary restrictions we need to lift). Except for a few hacks, Kakoune should not have any terminal specific knowledge outside of ncurses_ui.cc.

We can do the work progressively on master, fix whats detected as broken, and once ncurses_ui does not use ncurses anymore, rename it.

I support this approach as well.

A spreadsheet compiling support for escape sequences in modern terminal:
https://docs.google.com/spreadsheets/d/19W-lXWS9jYwqCK-LwgYo31GucPPxYVld_hVEcfpNpXg/edit#gid=1724051764

If you're going to make a list of terminals and their escape sequences, maybe make it a wiki page?

Apart from the terminals mentioned, rc/base/x11.kak also mentions alacritty and mintty (I assume that's the correct mintty).

It's also worth pointing out that the terminfo database that ships with ncurses is a wealth of information - both for the actual tested descriptions of what sequences are used by which terminals, but also the long and in-depth comments about individual terminals and terminal emulation in general - nearly 50% of the file is comments.

I'm not the original author of this document, this spreadsheet was recently shared on lobste.rs by JustinMK, the Neovim leadev.

The builtin-terminal-input branch is implementing this, I am keen to merge it but would like a bit of testing on setup different from mine (different terminal emulators mostly).

@mawww I can test it for [kitty].

seems to work in GNOME Terminal

That has been done for a while.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

notramo picture notramo  路  3Comments

alexherbo2 picture alexherbo2  路  3Comments

lenormf picture lenormf  路  4Comments

dpc picture dpc  路  4Comments

lenormf picture lenormf  路  4Comments