-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
Add cursor movement to IO.ANSI #7396
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldnt we be consistent and name the function "cursor_forth"?
Or use left and right instead? |
Or backward / forward |
Good catch @eksperimental. According to http://ascii-table.com/ansi-escape-sequences-vt-100.php what I called In ECMA-48 |
Then I'm all for up, down, right, left |
lib/elixir/lib/io/ansi.ex
Outdated
@spec cursor(integer, integer) :: String.t() | ||
def cursor(line, col), do: "\e[#{line};#{col}H" | ||
|
||
@doc "Sends cursor up." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to mention the argument in the docs.
Sends cursor lines
up
lib/elixir/lib/io/ansi.ex
Outdated
@spec cursor_up(integer) :: String.t() | ||
def cursor_up(lines), do: "\e[#{lines}A" | ||
|
||
@doc "Sends cursor down." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sends cursor lines
down.
lib/elixir/lib/io/ansi.ex
Outdated
|
||
@doc "Sends cursor left." | ||
@spec cursor_left(integer) :: String.t() | ||
def cursor_left(cols), do: "\e[#{cols}C" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally i dont like abbreviations in arguments. I would go with "columns"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They read better (and more natural) when you use them in the docs
lib/elixir/lib/io/ansi.ex
Outdated
@doc "Sends cursor left." | ||
@spec cursor_left(integer) :: String.t() | ||
def cursor_left(cols), do: "\e[#{cols}C" | ||
|
There was a problem hiding this comment.
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.
lib/elixir/lib/io/ansi.ex
Outdated
@spec cursor_left(integer) :: String.t() | ||
def cursor_left(cols), do: "\e[#{cols}C" | ||
|
||
@doc "Sends cursor right." |
There was a problem hiding this comment.
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 right.
Additionaly we could add guard checks to the function integer arguments. And what about zero and negative integers.. whats the behaviour. |
cursor uses an absolute or a relative position? I think it should be documented |
What do you think about using IO.ANSI.cursor_up()
IO.ANSI.cursor_down()
IO.ANSI.cursor_left()
IO.ANSI.cursor_right()
IO.ANSI.cursor_up(2)
IO.ANSI.cursor_down(3)
IO.ANSI.cursor_left(4)
IO.ANSI.cursor_right(5) |
@fertapric it makes total sense |
ANSI will actually default to 1 for us if we don't pass an argument, i.e. "\e[A" moves 1 line up. I think that's the correct behavior with no argument for us as well. |
If you send the ANSI code for moving the cursor absolutely without any arguments it defaults to 0,0, but we already have this in |
lib/elixir/lib/io/ansi.ex
Outdated
@@ -170,6 +170,30 @@ defmodule IO.ANSI do | |||
@doc "Sends cursor home." | |||
defsequence(:home, "", "H") | |||
|
|||
@doc "Sends cursor to the absolute position specified by `lines`, `columns`, with 0,0 being the top left corner." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would split this line:
@doc """
Sends cursor to the absolute position specified by `lines`, `columns`,
with 0,0 being the top left corner.
"""
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw, it seems the formatter leaves that @doc
tag as is. Should the formatter try to convert @doc "..."
into @doc """ (new line) ... """
? Or at least emit a message?
end | ||
end | ||
|
||
test "cursor_up/1" do |
There was a problem hiding this comment.
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
:)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😁 so it is
lib/elixir/lib/io/ansi.ex
Outdated
|
||
@doc "Sends cursor `lines` up (defaults to 1)." | ||
@spec cursor_up(integer) :: String.t() | ||
def cursor_up(), do: "\e[A" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the @spec
will be assigned to cursor_up/0
and the specification won't be accurate. I see two options:
# Option 1
@spec cursor_up :: String.t()
def cursor_up, do: "\e[A"
@spec cursor_up(integer) :: String.t()
def cursor_up(lines) when lines > 0, do: "\e[#{lines}A"
# Option 2
@spec cursor_up(integer) :: String.t()
def cursor_up(lines \\ 1)
def cursor_up(1), do: "\e[A"
def cursor_up(lines) when lines > 0, do: "\e[#{lines}A"
I would go for "Option 2" (because it's what the documentation says) :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like option 1 is more faithful to the underlying implementation which might matter in the future since terminal emulators can behave in strange ways.
lib/elixir/lib/io/ansi.ex
Outdated
def cursor_up(), do: "\e[A" | ||
|
||
@spec cursor_up(integer) :: String.t() | ||
def cursor_up(lines) when lines > 0, do: "\e[#{lines}A" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Last thing, promise 😅
The docs are now assigned to cursor_up/0
, leaving cursor_up/1
without any documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually thanks for all this feedback, as you can tell it's been a while and my elixir is a little rusty :)
is there a normal way to solve this problem? Or are you expected just to duplicate docs for multi-arity functions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multi-arity functions usually behave somehow different, so their documentation is usually not the same. There are some example in the Elixir codebase, Path.absname/1
and Path.absname/2
could be one of them.
@mveytsman is there any practical advantage in using |
@whatyouhide that's a fair point. Looking around, I don't see "\e[A" documented very much other than in the standard, so moving to My thinking was that someone may be dealing with a "weird" terminal and want to send the "\e[A" directly but in that case they can just do it themselves... (saving a byte is also definitely not worth uglier code 😬 ) |
Wouldn't the terminal be technically not working if |
lib/elixir/lib/io/ansi.ex
Outdated
@spec cursor(integer, integer) :: String.t() | ||
def cursor(line, column) when line >= 0 and column >= 0, do: "\e[#{line};#{column}H" | ||
|
||
@doc "Sends cursor `lines` up. Defaults to 1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need to mention the default, as \\
is standard in Elixir. Let's just have
@doc "Sends cursor `lines` lines up.
lib/elixir/lib/io/ansi.ex
Outdated
with 0,0 being the top left corner. | ||
""" | ||
@spec cursor(integer, integer) :: String.t() | ||
def cursor(line, column) when line >= 0 and column >= 0, do: "\e[#{line};#{column}H" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to check for the arguments being also integers, you should explicitly have a is_integer/1
check. >
and >=
works for floats as well.
Please do this on all other functions as well :).
lib/elixir/lib/io/ansi.ex
Outdated
@@ -170,6 +170,29 @@ defmodule IO.ANSI do | |||
@doc "Sends cursor home." | |||
defsequence(:home, "", "H") | |||
|
|||
@doc """ | |||
Sends cursor to the absolute position specified by `lines`, `columns`, | |||
with 0,0 being the top left corner. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first line of the documentation should be a summary of the function. Let's reshape this to be:
@doc """
Sends cursor to the absolute position specified by `line` and `column`.
Line `0` and column `0` would mean the top left corner.
"""
lib/elixir/lib/io/ansi.ex
Outdated
""" | ||
@spec cursor(integer, integer) :: String.t() | ||
def cursor(line, column) when line >= 0 and column >= 0, do: "\e[#{line};#{column}H" | ||
def cursor(line, column) | ||
when is_integer(line) and line >= 0 and is_integer(column) and column >= 0, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
assert IO.ANSI.cursor_up() == "\e[1A" | ||
assert IO.ANSI.cursor_up(12) == "\e[12A" | ||
|
||
assert_raise FunctionClauseError, fn -> |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
? :)
There was a problem hiding this comment.
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()
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work
end | ||
end | ||
|
||
test "cursor_right/0" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cursor_right/1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice first commit! 🎉 🎊 💚
Thanks @mveytsman! 💟 |
I added the cursor movement escape codes (from https://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x361.html) to
IO.ANSI