Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get cursor position in command line (Elixir)

Tags:

elixir

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!

like image 712
Nikolay Slavov Avatar asked Oct 09 '17 20:10

Nikolay Slavov


3 Answers

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

like image 116
Tarun Lalwani Avatar answered Nov 08 '22 05:11

Tarun Lalwani


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

like image 1
Kociamber Avatar answered Nov 08 '22 06:11

Kociamber


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.

like image 1
hedgesky Avatar answered Nov 08 '22 04:11

hedgesky