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
27 changes: 27 additions & 0 deletions lib/elixir/lib/io/ansi.ex
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,33 @@ 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(non_neg_integer, non_neg_integer) :: String.t()
def cursor(line, column)
when is_integer(line) and line >= 0 and is_integer(column) and column >= 0 do
"\e[#{line};#{column}H"
end

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

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

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

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

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

Expand Down
65 changes: 65 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,69 @@ 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(0)
end

assert_raise FunctionClauseError, fn ->
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_right(0)
end

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(0)
end

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

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

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

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