Hello, I'm working on a terminal app.
I want to get the cursor position so that I can teach illwill to create a buffer below the prompt (kind of like how fzf works). (more on this here: https://github.com/johnnovak/illwill/issues/41). I'm having trouble because when I ask the terminal for my cursor's position, it doesn't communicate directly to my nim app, but instead displays it to the user.
Here's some python, which works:
import sys
import termios
import tty
stdin_fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(stdin_fd)
response = ''
try:
tty.setraw(sys.stdin.fileno())
sys.stdout.write("\033[6n")
sys.stdout.flush()
while True:
ch = sys.stdin.read(1)
response += ch
if ch == 'R':
break
finally:
termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings)
# Parse the response to extract the row and column
parts = response.split('[')
row, col = parts[1].split(';')
print(int(row), int(col[:-1]))
My attempt at doing the same in nim:
import os, strutils
proc getCursorPosition(): tuple[row, col: int] =
var response = ""
var ch: char
write(stdout, "\x1b[6n")
flushFile(stdout)
while true:
let n = readBuffer(stdin, addr ch, 1)
if n == 0:
break
response.add(ch)
if ch == 'R':
break
let parts = response.split('[')
let rowCol = parts[1].split(';')
result = (parseInt(rowCol[0]), parseInt(rowCol[1][0..^2]))
let cursorPosition = getCursorPosition()
echo cursorPosition
When I run it, the terminal prints the data I need for the user to see, but nim just hangs there attempting to read it until I press enter
$ ./size
^[[49;1R # hangs here until I press enter
(row: 49, col: 1)
The notable difference is the lack of this stuff
try:
tty.setraw(sys.stdin.fileno())
# do stuff in raw mode
finally:
termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings)
Poking around the code for terminal, I see that there's already a procedure for this:
proc getCursorPos(h: Handle): tuple [x, y: int] =
But it's not documented and it's inside a when defined(windows) block, so I can't use it from Linux. What's the best way forward here? I'm not sure if I'm terminal-savvy enough to contribute a linux-side implementation (although I'm considering trying).
I've been meaning to figure out how to interop with Rust, so maybe I can use the FFI to talk with https://docs.rs/termion/latest/termion/ to get this data?
Or maybe there's an obvious solution that I'm overlooking. Thanks for talking a look.
Maybe wrap cfmakeraw from https://man7.org/linux/man-pages/man3/termios.3.html
No Rust wrapping required, that's crazy talk as the Rust code doesn't do but use the Linux/Posix APIs either.
NOTE: You may need to get _BSD_SOURCE or _GNU_SOURCE C #define 'd on Linux to implement Araq's fine suggestion. nim c --passC:-D_GNU_SOURCE is one way, but you could also do a Nim pragma in the source code (like {. passC: "..." .}).
Alternatively, cfmakeraw does very little { as Araq often says about "almost all C code" :-) }. Check out the (like 9 lines of code in py3.11) pure Python (re)implementation of setraw in /usr/lib/pythonX.Y/tty.py.
Nim's lib/posix/termios.nim already seems to have all the POSIX termios constants defined. So, it should be straightforward to just port Python's implementation of setraw. I think all you really need to know is that in Nim those bitwise-&|~ operators are instead the words and, or, not.
I was mistaken earlier.`terminal.setRaw` exists, it's just not public.
While tinkering with this I ended up implementing getCursorPos here: https://github.com/nim-lang/Nim/pull/22749
Would that be a reasonable addition to the standard library, or shall I just handle it on my side?
I also noticed that after my suggestion. :) FYI - even with no PR you can just import terminal {.all.} on non-Windows. E.g.:
import terminal {.all.}
echo setRaw.repr
Maybe you know that already, too. You do expose yourself to recompilation trouble if that part of the stdlib ever changes much, but that is unlikely in this case, IMO.
In terms of a PR exporting it, while I don't want to discourage you, my expectation is that the issue would mostly be that it would be guarded by a when not defined(windows):. Much of the stdlib is sort of a "portability shim" to write OS-agnostic code. You could still make setRaw a no-op on Windows or just make callers always add their own when guards. I don't want to discourage you from starting a PR - I am just anticipating a sticking point.
I appreciate the heads up. I'm very new to nim, so it's probably best to not assume I know anything. That {.all.} trick totally unblocks me. I'm now set to proceed with my app without making any changes here.
Still, it would be nice to contribute, supposing others would find this capability useful also.
Based on both the with and without windows sides of this, it looks quite easy to implement the same procedure signature on both sides. (I need to dig up a windows machine to test this belief, but I have updated that PR to show what I mean).
Thanks for the guidance.