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

Conversation

mveytsman
Copy link

I added the cursor movement escape codes (from https://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x361.html) to IO.ANSI

Copy link
Contributor

@eksperimental eksperimental left a 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"?

@eksperimental
Copy link
Contributor

Or use left and right instead?

@eksperimental
Copy link
Contributor

Or backward / forward

@mveytsman
Copy link
Author

Good catch @eksperimental.

According to http://ascii-table.com/ansi-escape-sequences-vt-100.php what I called cursor_forward is CUF and what I called cursor_back (oops!) is CUB.

In ECMA-48 CUF is called "CURSOR RIGHT" and CUB is "CURSOR LEFT", so calling the functions cursor_left and cursor_right would be more consistent

@eksperimental
Copy link
Contributor

Then I'm all for up, down, right, left
Thank you

@spec cursor(integer, integer) :: String.t()
def cursor(line, col), do: "\e[#{line};#{col}H"

@doc "Sends cursor up."
Copy link
Contributor

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

@spec cursor_up(integer) :: String.t()
def cursor_up(lines), do: "\e[#{lines}A"

@doc "Sends cursor down."
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 lines down.


@doc "Sends cursor left."
@spec cursor_left(integer) :: String.t()
def cursor_left(cols), do: "\e[#{cols}C"
Copy link
Contributor

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"

Copy link
Contributor

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

@doc "Sends cursor left."
@spec cursor_left(integer) :: String.t()
def cursor_left(cols), do: "\e[#{cols}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.

@spec cursor_left(integer) :: String.t()
def cursor_left(cols), do: "\e[#{cols}C"

@doc "Sends cursor right."
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 right.

@eksperimental
Copy link
Contributor

Additionaly we could add guard checks to the function integer arguments. And what about zero and negative integers.. whats the behaviour.

@eksperimental
Copy link
Contributor

cursor uses an absolute or a relative position? I think it should be documented

@fertapric
Copy link
Member

What do you think about using 1 as default value for all the functions?

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)

@eksperimental
Copy link
Contributor

@fertapric it makes total sense

@mveytsman
Copy link
Author

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.

@mveytsman
Copy link
Author

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 IO.ANSI.home/0 so I think it's okay to have IO.ANSI.cursor/2 require 2 arguments.

@@ -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."
Copy link
Member

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.
"""

Copy link
Member

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
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


@doc "Sends cursor `lines` up (defaults to 1)."
@spec cursor_up(integer) :: String.t()
def cursor_up(), do: "\e[A"
Copy link
Member

@fertapric fertapric Feb 26, 2018

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) :)

Copy link
Author

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.

def cursor_up(), do: "\e[A"

@spec cursor_up(integer) :: String.t()
def cursor_up(lines) when lines > 0, do: "\e[#{lines}A"
Copy link
Member

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.

Copy link
Author

@mveytsman mveytsman Feb 26, 2018

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?

Copy link
Member

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.

@whatyouhide
Copy link
Member

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.

@mveytsman is there any practical advantage in using \e[A instead of \e1[A, or any difference in general? If there isn't and it's just a "standard" think, personally I'd rather go with cursor_up(columns \\ 1) so the code and documentation are a bit more compact.

@mveytsman
Copy link
Author

mveytsman commented Feb 27, 2018

@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 \\ 1 makes sense.

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 😬 )

@whatyouhide
Copy link
Member

someone may be dealing with a "weird" terminal and want to send the "\e[A"

Wouldn't the terminal be technically not working if \e[A wasn't the same as \e1[A? Anyways if something like this ever happens, we'll deal with it when the bug report comes in :)

@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"
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 need to mention the default, as \\ is standard in Elixir. Let's just have

@doc "Sends cursor `lines` lines up.

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"
Copy link
Member

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 :).

@@ -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.
Copy link
Member

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.
"""

"""
@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,
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

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

Copy link
Contributor

@eksperimental eksperimental left a 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
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

Copy link
Member

@fertapric fertapric left a comment

Choose a reason for hiding this comment

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

Nice first commit! 🎉 🎊 💚

@whatyouhide whatyouhide merged commit d94a9cc into elixir-lang:master Feb 28, 2018
@whatyouhide
Copy link
Member

Thanks @mveytsman! 💟

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

6 participants