summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorMatt Rixman <5834582+MatrixManAtYrService@users.noreply.github.com>2023-10-08 16:56:18 +0000
committerGitHub <noreply@github.com>2023-10-08 18:56:18 +0200
commit5bcea05cafb2a655ed1dd44544998eac5bed7c7f (patch)
tree619763a0651a20cab19a5d1192d193c33b407a2e
parentefa64aa49b11e93461626e84b9f67b6185f26e1f (diff)
downloadNim-5bcea05cafb2a655ed1dd44544998eac5bed7c7f.tar.gz
Add getCursorPos() to std/terminal (#22749)
This would be handy for making terminal apps which display content below
the prompt (e.g. `fzf` does this).

Need to test it on windows before I remove "draft" status.

---------

Co-authored-by: Matt Rixman <MatrixManAtYrService@users.noreply.github.com>
Co-authored-by: Andreas Rumpf <rumpf_a@web.de>
-rw-r--r--lib/pure/terminal.nim48
1 files changed, 47 insertions, 1 deletions
diff --git a/lib/pure/terminal.nim b/lib/pure/terminal.nim
index 4177eb002..da95ac32a 100644
--- a/lib/pure/terminal.nim
+++ b/lib/pure/terminal.nim
@@ -60,7 +60,7 @@ runnableExamples("-r:off"):
 
 import macros
 import strformat
-from strutils import toLowerAscii, `%`
+from strutils import toLowerAscii, `%`, parseInt
 import colors
 
 when defined(windows):
@@ -96,6 +96,7 @@ const
   fgPrefix = "\e[38;2;"
   bgPrefix = "\e[48;2;"
   ansiResetCode* = "\e[0m"
+  getPos = "\e[6n"
   stylePrefix = "\e["
 
 when defined(windows):
@@ -220,6 +221,9 @@ when defined(windows):
       raiseOSError(osLastError())
     return (int(c.dwCursorPosition.x), int(c.dwCursorPosition.y))
 
+  proc getCursorPos*(): tuple [x, y: int] {.raises: [ValueError, IOError, OSError].} =
+    return getCursorPos(getStdHandle(STD_OUTPUT_HANDLE))
+
   proc setCursorPos(h: Handle, x, y: int) =
     var c: COORD
     c.x = int16(x)
@@ -267,6 +271,48 @@ else:
     mode.c_cc[VTIME] = 0.cuchar
     discard fd.tcSetAttr(time, addr mode)
 
+  proc getCursorPos*(): tuple [x, y: int] {.raises: [ValueError, IOError].} =
+    ## Returns cursor position (x, y)
+    ## writes to stdout and expects the terminal to respond via stdin
+    var
+      xStr = ""
+      yStr = ""
+      ch: char
+      ct: int
+      readX = false
+
+    # use raw mode to ask terminal for cursor position
+    let fd = getFileHandle(stdin)
+    var oldMode: Termios
+    discard fd.tcGetAttr(addr oldMode)
+    fd.setRaw()
+    stdout.write(getPos)
+    flushFile(stdout)
+
+    try:
+      # parse response format: [yyy;xxxR
+      while true:
+        let n = readBuffer(stdin, addr ch, 1)
+        if n == 0 or ch == 'R':
+          if xStr == "" or yStr == "":
+            raise newException(ValueError, "Got character position message that was missing data")
+          break
+        ct += 1
+        if ct > 16:
+          raise newException(ValueError, "Got unterminated character position message from terminal")
+        if ch == ';':
+          readX = true
+        elif ch in {'0'..'9'}:
+          if readX:
+            xStr.add(ch)
+          else:
+            yStr.add(ch)
+    finally:
+      # restore previous terminal mode
+      discard fd.tcSetAttr(TCSADRAIN, addr oldMode)
+
+    return (parseInt(xStr), parseInt(yStr))
+
   proc terminalWidthIoctl*(fds: openArray[int]): int =
     ## Returns terminal width from first fd that supports the ioctl.