Skip to content

Add cursor movement to IO.ANSI #7396

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Feb 28, 2018
26 changes: 26 additions & 0 deletions lib/elixir/lib/io/ansi.ex
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,32 @@ defmodule IO.ANSI do
@doc "Sends cursor home."
defsequence(:home, "", "H")

@doc """
Sends cursor to the absolute position specified by `line` and `column`.

Line `0` and column `0` would mean the top left corner.
"""
@spec cursor(integer, integer) :: String.t()
def cursor(line, column)
when is_integer(line) and line >= 0 and is_integer(column) and column >= 0,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@whatyouhide this is how mix format formatted it but it looks a little ugly. Think I should make a is_positive_integer guard / do we have one somewhere?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't but this formatting is fine. When when gets split on its own line, we just use the do/end syntax:

def cursor(line, column) 
    when is_integer(line) and line >= 0 and is_integer(column) and column >= 0 do
  ...
end

do: "\e[#{line};#{column}H"

@doc "Sends cursor `lines` up."
@spec cursor_up(integer) :: String.t()
def cursor_up(lines \\ 1) when is_integer(lines) and lines > 0, do: "\e[#{lines}A"

@doc "Sends cursor `lines` down."
@spec cursor_down(integer) :: String.t()
def cursor_down(lines \\ 1) when is_integer(lines) and lines > 0, do: "\e[#{lines}B"

@doc "Sends cursor `columns` to the left."
@spec cursor_left(integer) :: String.t()
def cursor_left(columns \\ 1) when is_integer(columns) and columns > 0, do: "\e[#{columns}C"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sends cursor columns to the left.

@doc "Sends cursor `columns` to the right."
@spec cursor_right(integer) :: String.t()
def cursor_right(columns \\ 1) when is_integer(columns) and columns > 0, do: "\e[#{columns}D"

@doc "Clears screen."
defsequence(:clear, "2", "J")

Expand Down
49 changes: 49 additions & 0 deletions lib/elixir/test/elixir/io/ansi_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,53 @@ defmodule IO.ANSITest do
IO.ANSI.color_background(5, -1, 1)
end
end

test "cursor/2" do
assert IO.ANSI.cursor(0, 0) == "\e[0;0H"
assert IO.ANSI.cursor(11, 12) == "\e[11;12H"

assert_raise FunctionClauseError, fn ->
IO.ANSI.cursor(-1, 5)
end

assert_raise FunctionClauseError, fn ->
IO.ANSI.cursor(5, -1)
end
end

test "cursor_up/1" do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also testing cursor_up/0 :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😁 so it is

assert IO.ANSI.cursor_up() == "\e[1A"
assert IO.ANSI.cursor_up(12) == "\e[12A"

assert_raise FunctionClauseError, fn ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also test the value 0? it seems a sensitive value because in an alternative implementation could be interpreted as \e[A or \e[0A. \e[0A works on my terminal and it's equivalent to \e[A or \e[1A.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Wouldn't it be best to have a guard that the value is >= 1? :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we go that way, the typespec should use pos_integer()

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the guard as > 0 but >= 1 is clearer. I changed that and the typespecs

IO.ANSI.cursor_up(-1)
end
end

test "cursor_down/1" do
assert IO.ANSI.cursor_down() == "\e[1B"
assert IO.ANSI.cursor_down(2) == "\e[2B"

assert_raise FunctionClauseError, fn ->
IO.ANSI.cursor_down(-1)
end
end

test "cursor_left/1" do
assert IO.ANSI.cursor_left() == "\e[1C"
assert IO.ANSI.cursor_left(3) == "\e[3C"

assert_raise FunctionClauseError, fn ->
IO.ANSI.cursor_left(-1)
end
end

test "cursor_right/0" do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cursor_right/1

assert IO.ANSI.cursor_right() == "\e[1D"
assert IO.ANSI.cursor_right(4) == "\e[4D"

assert_raise FunctionClauseError, fn ->
IO.ANSI.cursor_right(-1)
end
end
end