11

I'm wondering if there is a way of capturing the absolute cursor position in command line from Elixir.

I know that I have to use the following ansi escape sequence \033[6n, and after executing:

echo -en "\033[6n" 

prints exactly what I'm looking for, but I'm not sure how to get the command response from Elixir.

Thanks!

Peer Stritzinger
  • 8,232
  • 2
  • 30
  • 43
Nikolay Slavov
  • 185
  • 1
  • 7
  • A way of getting a command response in erlang is explained in [How to execute system command in erlang and get results - unreliable os:cmd/1](https://stackoverflow.com/questions/27028486/how-to-execute-system-command-in-erlang-and-get-results-unreliable-oscmd-1) but you'd have to _translate_ it to elixir – Juanjo Martin Oct 12 '17 at 10:18

4 Answers4

5

This one nearly drove me mad, I had to dig so many threads that I can't tell. I will add all the threads that are relevant to the solution, they are all worth a read.

So first thing first, we can't use System.cmd, System.cmd is run without a tty

iex(1)> System.cmd("tty", [])
{"not a tty\n", 1}

What we are trying to do requires a TTY. So there are few interesting libraries for the same

https://github.com/alco/porcelain

But that also doesn't work with a TTY

iex(1)> Porcelain.shell("tty")
%Porcelain.Result{err: nil, out: "not a tty\n", status: 1}

Then came to another library

https://github.com/aleandros/shell_stream

This one seems to share the TTY

iex(3)> ShellStream.shell("tty") |> Enum.to_list
["/dev/pts/6"]

This TTY is the same as the TTY of the current terminal, which means the TTY is propagating to the child process

Next was to check if we could get the coordinates

iex(8)> ShellStream.shell("echo -en '033[6n'") |> Enum.to_list
[]

So after lots of hit and trial I came up with a approach

defmodule CursorPos do

  def get_pos do
      settings = ShellStream.shell("stty -g") |> Enum.to_list

      #ShellStream.shell("stty -echo -echoctl -imaxbel -isig -icanon min 1 time 0")
      ShellStream.shell("stty raw -echo")
      #settings |> IO.inspect
      spawn(fn ->
            IO.write "\e[6n"
            #ShellStream.shell "echo -en \"\033[6n\" > `tty`"
            :timer.sleep(50)
            IO.write "\n"
           end)
      io = IO.stream(:stdio,1)
      data = io |> Stream.take_while(&(&1 != "R"))
      data|> Enum.join  |> IO.inspect
      ShellStream.shell("stty #{settings}")
  end

  def main(args) do
      get_pos
  end
end

This kind of works but still needs you to press enter to read stdio

$ ./cursorpos
^[[24;1R

"\e[24;1"

It also alters the screen coordinates to get them, which is not what one would like. But the issue is that the coordinate control characters needs to be processed by your shell and not the child shell. My attempt to use

ShellStream.shell("stty -echo -echoctl -imaxbel -isig -icanon min 1 time 0")

Doesn't work as the stty is not affecting the parent shell where we need to get the coordinates. So next possible solution is to do below

$ EXISTING=$(stty -g);stty -echo -echonl -imaxbel -isig -icanon min 1 time 0; ./cursorpos ; stty $EXISTING
"\e[24;1"

This works because we are able to alter the properties of the current tty. Now you may want to dig deeper and find how you could do that from inside the code.

I have put all my code into below Github project

https://github.com/tarunlalwani/elixir-get-current-cursor-position

Also you should look at below project

https://github.com/henrik/progress_bar

If your getting cursor position is not important then you can fix the cursor yourself to some position.

References

https://unix.stackexchange.com/questions/88296/get-vertical-cursor-position

How to get the cursor position in bash?

https://groups.google.com/forum/#!topic/elixir-lang-talk/9B1oe3KgjnE

https://hexdocs.pm/elixir/IO.html#getn/3

https://unix.stackexchange.com/questions/264920/why-doesnt-the-enter-key-send-eol

http://man7.org/linux/man-pages/man1/stty.1.html

https://github.com/jfreeze/ex_ncurses

How can I get the cursor's position in an ANSI terminal?

Get console user input as typed, char by char

Tarun Lalwani
  • 142,312
  • 9
  • 204
  • 265
  • `spawn` the process that writes and make it sleep! Of course! Nice. – Aleksei Matiushkin Oct 18 '17 at 06:55
  • Thanks for looking into this. If the user needs to press enter though, is this any better than just `IO.puts("\e[6n"); IO.gets("")`? – Dogbert Oct 18 '17 at 08:47
  • @Dogbert, Not in the final case I posted `$ EXISTING=$(stty -g);stty -echo -echonl -imaxbel -isig -icanon min 1 time 0; ./cursorpos ; stty $EXISTING "\e[24;1"`. As you can see this output is from our `inspect` – Tarun Lalwani Oct 18 '17 at 09:27
1

The closest thing I've managed to achieve was this line:

~C(bash -c "echo -en '\033[6n'") |> :os.cmd

However, it returns '\e[6n' instead of cursor position. Must be something with escaping symbols inside :os.cmd/1 function, don't know for sure.

Though it's not a complete answer, hope it helps anyway.

hedgesky
  • 3,271
  • 1
  • 21
  • 36
1

I know it's weird macro stuff but it works! ;) I have discovered it when I was investigating an implementation of IO.ANSI.Sequence.home() function here.

Special thanks to one and only Thijs

defmodule Foo do                          
  import IO.ANSI.Sequence                   
  IO.ANSI.Sequence.defsequence :bar, 6, "n" 
end

and then simply call: IO.puts Foo.bar

Kociamber
  • 1,048
  • 8
  • 16
  • This is exactly the same as `def bar, do: "\e[6n"`. The question is about getting the value the terminal prints in response into a variable. – Dogbert Oct 17 '17 at 14:44
  • ```def bar, do: "\e[6n"``` seems to be returning a string only while my module ```:ok``` atom and cursor position (at least when I'm running it in my iex). The position can be captured by IO.gets I guess: ```var = IO.gets Foo.bar``` – Kociamber Oct 18 '17 at 08:39
  • With that `def bar`, you need to do `IO.puts Foo.bar` as well. Unfortunately IO.gets doesn't work to read that output. :( – Dogbert Oct 18 '17 at 08:42
  • I've just tried it and received ```"4;1R\n"``` - so we are almost there, it just needs re-formatting ;) – Kociamber Oct 18 '17 at 08:44
  • Unfortunately yes. I will check what can we do about it a bit later today. – Kociamber Oct 18 '17 at 10:13
  • The only way I can see to achieve this would be to write our own IO.puts implementation and pass the result to variable instead of stdout. – Kociamber Oct 19 '17 at 08:29
-1

If you know the system command, use System module to execute it from Elixir

Guy Yogev
  • 861
  • 5
  • 14