logo

drewdevault.com

[mirror] blog and personal website of Drew DeVault git clone https://hacktivis.me/git/mirror/drewdevault.com.git

Line-printer-shell-hack.md (7561B)


  1. ---
  2. date: 2019-10-30
  3. layout: post
  4. title: An old-school shell hack on a line printer
  5. tags: [hack]
  6. ---
  7. It's been too long since I last did a good hack, for no practical reason other
  8. than great hack value. In my case, these [often amount][vt220] to a nostalgia
  9. for an age of computing I wasn't present for. In a recent bid to capture more of
  10. this nostalgia, I recently picked up a dot matrix line printer, specifically the
  11. Epson LX-350 printer. This one is nice because it has a USB port, so I don't
  12. have to break out my pile of serial cable hacks to get it talking to Linux 😁
  13. [vt220]: https://drewdevault.com/2016/03/22/Integrating-a-VT220-into-my-life.html
  14. This is the classic printer style, with infinite paper and a lovely noise during
  15. printing. They are also fairly simple to operate - you can just write text
  16. directly to `/dev/lp` (or `/dev/usb/lp9` in my case) and it'll print it out.
  17. Slightly more sophisticated instructions can be written to them with ANSI escape
  18. sequences, just like a terminal. They can also be rigged up to CUPS, then you
  19. can use something like `man -t 5 scdoc` to produce printouts like this:
  20. [![](https://sr.ht/gHCA.jpg)](https://sr.ht/gHCA.jpg)
  21. Plugging the printer into Linux and writing out pages isn't much for hack value,
  22. however. What I really wanted to make was something resembling an old-school
  23. TTY - teletypewriter. So I wrote some [glue code in
  24. Golang](https://git.sr.ht/~sircmpwn/lpsh), and soon enough I had a shell:
  25. <iframe width="560" height="315" sandbox="allow-same-origin allow-scripts
  26. allow-popups"
  27. src="https://spacepub.space/videos/embed/d8943b2d-8280-497b-85ec-bc282ec2afdc"
  28. frameborder="0" allowfullscreen style="width: 100%"></iframe>
  29. The glue code I wrote for this is fairly straightforward. In the simplest form,
  30. it spins up a pty (pseudo-terminal), runs `/bin/sh` in it, and writes the pty
  31. output into the line printer device. For those unaware, a pseudo-terminal is the
  32. key piece of software infrastructure for running interactive text applications.
  33. Applications which want to do things like print colored text, move
  34. the cursor around and draw a TUI, and so on, will open `/dev/tty` to open the
  35. current TTY device. For most applications used today, this is a
  36. "pseudo-terminal", or pty, which is a terminal emulated in userspace - i.e. by
  37. your terminal emulator. However, your terminal emulator is *emulating* a
  38. terminal - the control sequences applications send to these are
  39. backwards-compatible with 50 years of computing history. Interfaces like these
  40. are the namesake of the TTY.
  41. Visual terminals came onto the scene later on, and in the classic computing
  42. tradition, the old hands complained that it was less useful - you could no
  43. longer write notes on your backlog, tear off a page and hand it to a colleague,
  44. or [white-out](https://en.wikipedia.org/wiki/Wite-Out) mistakes. Early
  45. [visual terminals](https://en.wikipedia.org/wiki/Computer_terminal) could also
  46. be plugged directly into a line printer, and you could configure them to echo to
  47. the printer or print out a screenfull of text at a time. A distinct advantage of
  48. visual terminals is not having to deal with so much bloody paper, a problem that
  49. I've become acutely familiar with in the past few days[^1].
  50. Getting back to the glue code, I chose Golang because setting up a TTY is a bit
  51. of a hassle in C, but in Golang it's pretty straightforward. There is a serial
  52. port and in theory I could have plugged it in and spawned a getty on the
  53. resulting serial device - but (1) it'd be write-only, so not especially
  54. interactive without *hardware* hacks, and (2) I didn't feel like digging out my
  55. serial cables. So:
  56. ```go
  57. import "git.sr.ht/~sircmpwn/pty" // fork of github.com/kr/pty
  58. // ...
  59. winsize := pty.Winsize{
  60. Cols: 160,
  61. Rows: 24,
  62. }
  63. cmd := exec.Command("/bin/sh")
  64. cmd.Env = append(os.Environ(),
  65. "TERM=lp",
  66. fmt.Sprintf("COLUMNS=%d", 180))
  67. tty, err := pty.StartWithSize(cmd, &winsize)
  68. ```
  69. *P.S. We're going to dive through the code in detail now. If you just want more
  70. cool videos of this in action, skip to the bottom.*
  71. I set the TERM environment variable to `lp`, for line printer, which doesn't
  72. really exist but prevents most applications from trying anything too tricksy
  73. with their escape codes. The `tty` variable here is an `io.ReadWriter` whose
  74. output is sent to the printer and whose input is sourced from wherever, in my
  75. case from the stdin of this process[^2].
  76. For a little more quality-of-life, I looked up Epson's proprietary ANSI escape
  77. sequences and found out that you can tell the printer to feed back and forth in
  78. 216th" increments with the j and J escape sequences. The following code will
  79. feed 2.5" out, then back in:
  80. ```go
  81. f.Write([]byte("\x1BJ\xD8\x1BJ\xD8\x1BJ\x6C"))
  82. f.Write([]byte("\x1Bj\xD8\x1Bj\xD8\x1Bj\x6C"))
  83. ```
  84. Which happens to be the perfect amount to move the last-written line up out of
  85. the printer for the user to read, then back in to be written to some more. A
  86. little bit of timing logic in a goroutine manages the transition between "spool
  87. out so the user can read the output" and "spool in to write some more output":
  88. ```go
  89. func lpmgr(in chan (interface{}), out chan ([]byte)) {
  90. // TODO: Runtime configurable option? Discover printers? dunno
  91. f, err := os.OpenFile("/dev/usb/lp9", os.O_RDWR, 0755)
  92. if err != nil {
  93. panic(err)
  94. }
  95. feed := false
  96. f.Write([]byte("\n\n\n\r"))
  97. timeout := 250 * time.Millisecond
  98. for {
  99. select {
  100. case <-in:
  101. // Increase the timeout after input
  102. timeout = 1 * time.Second
  103. case data := <-out:
  104. if feed {
  105. f.Write([]byte("\x1Bj\xD8\x1Bj\xD8\x1Bj\x6C"))
  106. feed = false
  107. }
  108. f.Write(lptl(data))
  109. case <-time.After(timeout):
  110. timeout = 200 * time.Millisecond
  111. if !feed {
  112. feed = true
  113. f.Write([]byte("\x1BJ\xD8\x1BJ\xD8\x1BJ\x6C"))
  114. }
  115. }
  116. }
  117. }
  118. ```
  119. `lptl` is a work-in-progress thing which tweaks the outgoing data for some
  120. quality-of-life changes, like changing backspace to ^H. Then, the main event
  121. loop looks something like this:
  122. ```go
  123. inch := make(chan (interface{}))
  124. outch := make(chan ([]byte))
  125. go lpmgr(inch, outch)
  126. inbuf := make([]byte, 4096)
  127. go func() {
  128. for {
  129. n, err := os.Stdin.Read(inbuf)
  130. if err != nil {
  131. panic(err)
  132. }
  133. tty.Write(inbuf[:n])
  134. inch <- nil
  135. }
  136. }()
  137. outbuf := make([]byte, 4096)
  138. for {
  139. n, err := tty.Read(outbuf)
  140. if err != nil {
  141. panic(err)
  142. }
  143. b := make([]byte, n)
  144. copy(b, outbuf[:n])
  145. outch <- b
  146. }
  147. ```
  148. The tty will echo characters written to it, so we just write to it from stdin
  149. and increase the form feed timeout closer to the user's input so that it's not
  150. constantly feeding in and out as you write. The resulting system is pretty
  151. pleasant to use! I spent about hour working on improvements to it on a [live
  152. stream](https://live.drewdevault.com). You can watch the system in action on the
  153. archive here:
  154. <iframe width="560" height="370" sandbox="allow-same-origin allow-scripts"
  155. src="https://spacepub.space/videos/embed/a8be6c87-9267-452e-8d3e-dd206880fa98"
  156. frameborder="0" allowfullscreen style="width: 100%"></iframe>
  157. If you were a fly on the wall when Unix was written, it would have looked a lot
  158. like this. And remember: [ed is the standard text
  159. editor](https://www.gnu.org/fun/jokes/ed-msg.html).
  160. ?
  161. [^1]: Don't worry, I recycled it all.
  162. [^2]: In the future I want to make this use libinput or something, or eventually make a kernel module which lets you pair a USB keyboard with a line printer to make a TTY directly. Or maybe a little microcontroller which translates a USB keyboard into serial TX and forwards RX to the printer. Possibilities!