summaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/blog/index.gsp1
-rw-r--r--src/blog/termios/disable-xoff-xon.diff16
-rw-r--r--src/blog/termios/enable-xoff-xon.diff47
-rw-r--r--src/blog/termios/index.gsp222
-rw-r--r--src/blog/termios/test.c21
5 files changed, 307 insertions, 0 deletions
diff --git a/src/blog/index.gsp b/src/blog/index.gsp
index 66e35de..36d2231 100644
--- a/src/blog/index.gsp
+++ b/src/blog/index.gsp
@@ -28,6 +28,7 @@ html lang="en" {
p {-Posts:}
ul {
+ ARTICLE(termios, {-Reading and Writing Terminal Attributes})
ARTICLE(windowing, {-Application Windowing is Bad})
ARTICLE(extend, {-Extensible Scripting})
ARTICLE(grab, {-Making Grep Better})
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 <curses.h>
+ #include <stdio.h>
++#include <termios.h>
++#include <unistd.h>
+ #include <vis.h>
+
+ 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 <curses.h>
+ #include <stdio.h>
++#include <stdlib.h>
+ #include <termios.h>
+ #include <unistd.h>
+ #include <vis.h>
+
++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 <curses.h>
+#include <stdio.h>
+#include <vis.h>
+
+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 */
+}