From 083b0b53e450f317ca6e99cc1d077b10a5ac7d4d Mon Sep 17 00:00:00 2001 From: Thomas Voss Date: Mon, 28 Oct 2024 02:18:06 +0100 Subject: Add a new article on termios --- src/blog/termios/disable-xoff-xon.diff | 16 +++ src/blog/termios/enable-xoff-xon.diff | 47 +++++++ src/blog/termios/index.gsp | 222 +++++++++++++++++++++++++++++++++ src/blog/termios/test.c | 21 ++++ 4 files changed, 306 insertions(+) create mode 100644 src/blog/termios/disable-xoff-xon.diff create mode 100644 src/blog/termios/enable-xoff-xon.diff create mode 100644 src/blog/termios/index.gsp create mode 100644 src/blog/termios/test.c (limited to 'src/blog/termios') diff --git a/src/blog/termios/disable-xoff-xon.diff b/src/blog/termios/disable-xoff-xon.diff new file mode 100644 index 0000000..482cca7 --- /dev/null +++ b/src/blog/termios/disable-xoff-xon.diff @@ -0,0 +1,16 @@ + #include + #include ++#include ++#include + #include + + int + main(void) + { ++ struct termios tp; ++ tcgetattr(STDIN_FILENO, &tp); ++ tp.c_iflag &= ~(IXANY | IXON | IXOFF); ++ tcsetattr(STDIN_FILENO, TCSANOW, &tp); ++ + initscr(); /* Init screen */ + noecho(); /* Don’t echo keypresses */ diff --git a/src/blog/termios/enable-xoff-xon.diff b/src/blog/termios/enable-xoff-xon.diff new file mode 100644 index 0000000..64841d2 --- /dev/null +++ b/src/blog/termios/enable-xoff-xon.diff @@ -0,0 +1,47 @@ + #include + #include ++#include + #include + #include + #include + ++static void restore_tcattrs(void); ++ ++struct termios old_attrs; ++ + int + main(void) + { + struct termios tp; + tcgetattr(STDIN_FILENO, &tp); ++ old_attrs = tp; + tp.c_iflag &= ~(IXANY | IXON | IXOFF); + tcsetattr(STDIN_FILENO, TCSANOW, &tp); ++ atexit(restore_tcattrs); + + initscr(); /* Init screen */ + noecho(); /* Don’t echo keypresses */ +@@ -25,19 +18,10 @@ + for (;;) { + /* Read char and encode it as a string */ + char buf[5]; ++ int ch = getch(); ++ if (ch == '\x11') /* ^Q */ ++ break; ++ vis(buf, ch, VIS_WHITE, 0); +- vis(buf, getch(), VIS_WHITE, 0); + + clear(); /* Clear screen */ + printw("%s\n", buf); /* Write char to screen */ + refresh(); /* Refresh display */ + } +- /* unreachable */ ++ endwin(); /* Destroy screen */ ++ return EXIT_SUCCESS; + } ++ ++void ++restore_tcattrs(void) ++{ ++ tcsetattr(STDIN_FILENO, TCSANOW, &old_attrs); ++} diff --git a/src/blog/termios/index.gsp b/src/blog/termios/index.gsp new file mode 100644 index 0000000..d1f4e48 --- /dev/null +++ b/src/blog/termios/index.gsp @@ -0,0 +1,222 @@ +html lang="en" { + head { HEAD } + body { + header { + div .head { + h1 {-Disabling Terminal Transmission Codes} + } + + figure .quote { + blockquote { + p {= + UNIX was not designed to stop its users from doing stupid + things, as that would also stop them from doing clever + things. + } + } + figcaption {-Doug Gwyn} + } + } + + main { + h2 #intro {-Introduction} + p {= + As part of my university studies which I have recently + restarted@sup{=MKREF(uni)}, I had to implement the popular dice + game @em{-Yatzy}@sup{=MKREF(yz)}. We were only expected to + implement a basic @code{-print()}/@code{-readline()} interface + since it’s an intro class and everyone is just learning to code + for the first time. + } + + p {= + Being the already well-experienced programmer I am, I decided to + take this opportunity to learn something new and decided to + implement a more fully-featured game with a + @a + href="https://en.wikipedia.org/wiki/Curses_%28programming_library%29" + target="_blank" + {-curses UI} + and various keyboard shortcuts for more efficient navigation of + the various game screens. + } + + p {= + This post is going to cover how I handled the keyboard shortcuts + @kbd{-Ctrl+Q} and @kbd{-Ctrl+S}, and how it isn’t as trivial as + you might first expect. For the sake of brevity I will be + refering to keychords of the form @kbd{-Ctrl+K} as @kbd{-^K} for + the remainder of this article. + } + + hr{} + aside data-ref="REF(uni)" {= + I dropped out of university 3 years ago after loosing interest in + the course I was taking at the time. I’ve made the decision to + continue again so that I can obtain a degree while the + opportunity still presents itself. + } + + aside data-ref="REF(yz)" {= + If you’re familiar with the game @em{-Yahtzee} then you might + think this is a typo — it’s not. Yatzy is a very similar game + to Yahtzee that is more commonly played in the nordic countries + (including my current country of residence, Sweden). + } + + h2 #hello-world {-A Simple Test} + p {= + In order to better describe the issue at hand, allow me to + provide you with a basic test program in C using the + aforementioned curses API. The following program@sup{=MKREF(cc)} + implements a basic loop where you can enter a key on your + keyboard and the corresponding character is displayed on the + terminal. Since we are interested in keyboard shortcuts + involving the control key we need a way to actually visualize + control characters. Luckily the + @a + href="https://man.openbsd.org/vis.3" + target="_blank" + {-@code{-vis(3)}}@sup{=MKREF(vis)} + family of functions exists to do just that. + } + + figure { FMT_CODE(test.c) } + + p {= + If you were to run the above example program you would get for + the most part, expected results. Pressing @kbd{-a} renders ‘a’, + and pressing @kbd{-^X} renders ‘\\^X’. There’s an issue though: + try to enter @kbd{-^S} and notice that what happens is …nothing! + Not only does nothing happen but the entire terminal freezes up. + The only way to @em{-unfreeze} the terminal is to hit @kbd{-^Q}. + } + + p {= + This is @em{-really} unfortunate because @kbd{-^Q} and @kbd{-^S} + are really useful mnemonic shortcuts: just think of ‘quit’, + ‘save’, and other common actions an application might want + shortcuts for. + } + + hr{} + aside data-ref="REF(cc)" {= + If you’re interested in building it for yourself, you will + probably need to pass @code{--lcurses} to your compiler to link + with curses. + } + + aside data-ref="REF(vis)" {= + Likewise here if you’re on a BSD-like system (including MacOS) + then you should be fine, but Linux users will need to pass the + output of @code{-pkg-config libbsd-overlay} to their compiler, + with both the @code{---cflags} and @code{---libs} flags. + } + + h2 #explanation {-What’s Going On?} + p {= + So what’s going on here is in simple terms, a historical relic of + times gone by. It just so happens that the @kbd{-^S} and + @kbd{-^Q} keys correspond to the XOFF and XON terminal + transmission codes, which stand for ‘transmit off’ and ‘transmit + on’. There may still be a modern usecase for these two + transmission codes, but from my surface-level understanding they + were mostly useful in the earlier years of computing where + devices were slower and had to tell input devices to stop sending + data to avoid getting overwhelmed. As a user of a terminal + emulator on a modern personal computer, this is totally useless + functionality. + } + + p {= + So now that we know what the problem is, we need to figure out + how to solve it. Luckily for us this is actually a lot easier + than you might expect thanks to two simple functions: + @a + href="https://linux.die.net/man/3/tcgetattr" + target="_blank" + {-@code{-tcgetattr(3)}} + and + @a + href="https://linux.die.net/man/3/tcsetattr" + target="_blank" + {-@code{-tcsetattr(3)}}. + } + + p {= + Here’s how we can disable the XOFF/XON codes completely, and + allow the user to input @kbd{-^S} and @kbd{-^Q}: + } + + figure { FMT_CODE(disable-xoff-xon.diff) } + + p {= + What’s going on here is actually really simple. We first get the + current terminal attributes for the standard input using + @code{-tcgetattr} and store them in a @code{-struct termios}. + This structure (amongst other things) has various bitmasks + describing the terminal modes including input-, output-, + control-, and local-modes. What most of those are is out of the + scope of this article but what we are interested in are the + @em{-input modes}. + } + + p {= + The input modes are stored in the @code{-c_iflag} member of the + @code{-struct termios} structure and there are various flags here + that we can toggle which are documented in the + @code{-tcgetattr(3)} manual. The three we’re interested in are + @code{-IXANY}, @code{-IXON}, and @code{-IXOFF}. These flags are + described as such by the aforementioned manual: + } + + dl { + dt { code {-IXANY} } + dd {-Typing any character will restart stopped output.} + dt { code {-IXON} } + dd {-Enable XON/XOFF flow control on output.} + dt { code {-IXOFF} } + dd {-Enable XON/XOFF flow control on input.} + } + + p {= + In other words… this is all stuff we want to turn off. Since we + are working with bitmasks we can unset the relevant bits by ORing + all the bits together, negating the mask, and doing a bitwise + AND; it’s all standard bitwise operations. + } + + p {= + The last thing we need to do is to take our modified attributes + and tell the terminal to use them. This is where + @code{-tcsetattr()} comes into play; we just give it our + attributes and file descriptor and we can now use @kbd{-^S} + without freezing our terminal! The only other thing to note is + the @code{-TCSANOW} flag — it just means that we want our + changes to take effect immediately. + } + + h2 #finish {-Finishing Touches} + p {= + What we’ve got here is almost all that we need, but we’re missing + one crucial part: when the user exits our program these settings + will still persist! Chances are that most users will never + realize this, but that doesn’t mean that we shouldn’t be good + engineers and clean up after ourselves. Luckily to do this we + don’t actually need to do anything new, we just need to keep + track of the original attributes when we first queried them: + } + + figure { FMT_CODE(enable-xoff-xon.diff) } + + p {= + With these simple changes, we can now use @kbd{-^S} and @kbd{-^Q} + as expected in our application but once we exit the application + with @kbd{-^Q} the XON/XOFF functionality of our terminal is + restored and behaves as expected. + } + } + + footer { FOOT } + } +} diff --git a/src/blog/termios/test.c b/src/blog/termios/test.c new file mode 100644 index 0000000..dcb93d1 --- /dev/null +++ b/src/blog/termios/test.c @@ -0,0 +1,21 @@ +#include +#include +#include + +int +main(void) +{ + initscr(); /* Init screen */ + noecho(); /* Don’t echo keypresses */ + + for (;;) { + /* Read char and encode it as a string */ + char buf[5]; + vis(buf, getch(), VIS_WHITE, 0); + + clear(); /* Clear screen */ + printw("%s\n", buf); /* Write char to screen */ + refresh(); /* Refresh display */ + } + /* unreachable */ +} -- cgit v1.2.3