Few questions about cli with crystal

Hi everyone!

As you know I am working on ISM, cli to manage a Linux system.

I am facing some smalls problem actually.

I would like to know if it’s possible when my program is running under TTY if it’s possible to print underlined characters (but I guess it’s not possible). If it’s not, how can I detect if the software is under a TTY and print differently characters?

The other thing is about to align items.

If for example in 3 different ligns I would like to align some text at the same column, is there any function / way to do that ?

Last question, if I print a sentence longer that the lign size, how can I manage with that ?
Because ISM use sometime animated text, but if the text is longer than the lign, the text don’t erase the previous one, it will be duplicate

In a Linux terminal emulator, this can be done with simple ANSI sequences. The standard library has the Colorize module which has some ANSI stuff already in it, where you can do something like:

STDOUT << "This text is " << "underlined".colorize.underline << " text\n"

Or with puts or printf or whatever you’d like.

Under the hood, it’s essentially equivalent to something like STDOUT << "this is \e[4munderlined\e[24m text\n", but you don’t have to remember the actual escape sequences and codes.

Aligning is probably best done with printf and field widths. The documentation is here and is very close (identical?) to printf in other languages.

For a sentence longer than the line size, I wrote my own wrapping function. The repo is here if you want to use it. I don’t think there’s something like this in the standard library.

1 Like

Okay thanks a lot for your answer, I will try that when I will be back home.

Just only one thing I think you didn’t answer, how can I detect if the running terminal is a terminal emulator or a TTY?

1 Like

That I’m not sure about. Technically a terminal is a TTY already.

If you want to distinguish between the TTY and the output to the file (> or | on command line), just call this on the beggining of the app:

Colorize.on_tty_only!

Doc: Colorize - Crystal 1.8.2

1 Like

There is IO#tty?.

1 Like

Did you have a look at the curses library?

Hi guys, I am coming back to you because I have an another question about the CLI I am coding, because I almost finished to code all of the main features for my project.

Actually my CLI play sometime a text animation. But actually it’s just basically that:

print text+"\r"

But I am not happy to proceed like that, because if the terminal is resized, or if you are with a small resolution, the terminal will print duplicate text.

I would like to know how I can clear properly the text.

How should I proceed ?

Look into ANSI Escape Codes · GitHub. ANSI Escape Codes will allow you to do what you want.

2 Likes

I am a bit lost.

If I would like to use that one:

I should do like that ? Because it doesn’t work:

print "TestESC[1J"

Or like that?

"Test\x1B[1J"

\x1B yea. It’s listed up above in ANSI Escape Codes · GitHub as in general code used in all the other ones.

So I am not sure the screenshot I shown is the solution.

The problem is, if the terminal is resized, or too small, how can I clean the right amout of characters ?

I need to do like a backspace, but prevent the CLI to duplicate the text if the terminal is resized

Nice, I didn’t know that and I needed it :-)

Wow! Valuable information here :slight_smile:

And about my question? XD

You might have to start tracking some state in your program since terminals aren’t going to be smart enough to know how to properly re-render text in all situations where the terminal size changes. You’ll have to track things like the current terminal size, how much you’ve printed, where the cursor is, etc…

On Unix platforms, you can get the terminal size like this (I don’t know if this got added to the stdlib since I originally wrote it, so it may already be present):

lib LibC
  {% if flag?(:unix) %}
    # Bare minimum for the ioctl stuff we need

    TIOCGWINSZ = 0x5413u32

    struct Winsize
      ws_row    : UInt16
      ws_col    : UInt16
      ws_xpixel : UInt16
      ws_ypixel : UInt16
    end

    fun ioctl(fd : LibC::Int, what : LibC::ULong, ...) : LibC::Int
  {% end %}
end

{% if flag?(:unix) %}
  # Gets the height and width of the terminal (in that order).
  def getWinSize : Tuple(UInt16, UInt16)
    thing = LibC::Winsize.new
    LibC.ioctl(STDOUT.fd, LibC::TIOCGWINSZ, pointerof(thing))
    {thing.ws_row, thing.ws_col}
  end
{% end %}

From there, you’d move the cursor around, deleting and re-printing lines or segments of text as-needed.

The nuclear option would be to keep track of everything printed, then if the terminal is resized, completely clear it and redraw it :stuck_out_tongue:

I have absolutely no data to back this up, but my gut tells me that a user is more likely to grow the size of the terminal, not shrink it, unless they’re in a tiling window manager.

3 Likes

So definitely it’s an answer. More tricky than I expected !

Okay I will put that on my list to do, but for later I think .

I didn’t think I will need to implement that. I thought there is a standard library for that

The only advantage I will have is my program is only intended to be for Linux, so I don’t need to check the platform

1 Like

The method I use to repaint the terminal and survive resizing is not perfect and can fail, but looks basically like this:

  1. detect a tty with STDOUT.tty? or STDERR.tty? depending on where I’m writing, and if it’s not a tty, use Log to write updates instead of any of these things.
  2. decide how many lines I need to use, and print that many linefeeds: print '\n' * num_lines
  3. when printing my progress info, first send "\e[%dA" % num_lines in order to move up to where I should be
  4. write my N lines of output, ending each line with "\e[K" to clear to end of line
  5. in theory–i’ve not tried it–it should be possible to catch a SIGWNCH signal and use it to re-request the terminal dimensions in order to repaint appropriately. however, this can definitely still leave unexpected output if the resize causes lines to wrap that were not wrapped before. if this is an important problem, then i’d use the alternate screen ("\e[?1049h" to move to the alternate screen, "\e[?1049l" to move back when exiting the program) and instead of using the "\e[A" sequence to move up N lines, use "\e[1;1H" to always move to the start of the first line, allowing me to use the whole terminal display for my output.
3 Likes

I was trying that the other day and the signal didn’t want to fire, but this does seem to be a bit more ideal than constantly checking the terminal size every screen update. Maybe it was the terminal I was using (Terminology), or I could have been doing something wrong.

1 Like

I plan to use this solution, because I would like to make something really cool. I will try to use that, and I will come back to you if I have any questions

1 Like