Escribir en la misma línea de la consola y obtener el ancho y alto de la terminal con Java

Escrito por el .
java planeta-codigo
Comentarios

Java

Algunas aplicaciones en su salida en la terminal muestran una barra de progreso para la cual necesitan utilizar la secuencia de escape de la terminal o el caracter de carro para posicionar el cursor al inicio de la línea. En algunos casos incluso se muestran varias barras de progreso. Estos son los casos de los gestores de paquetes de GNU/Linux como pacman al realizar una actualización del sistema o de Gradle al descargar las dependencias.

Con las secuencias de escape se pueden cambiar los colores de los caracteres tanto del propio caracter como el color de fondo. Otras aplicaciones como el reproductor de música cmus muestran en la terminal una interfaz basada en texto con un barra de estado y varios paneles con la lista de las canciones del tamaño que tenga la terminal. Para esto es necesario conocer cuál es el tamaño de la terminal de columnas a lo ancho y de filas a lo alto.

Hay varias formas de conocer el tamaño de la terminal. Con el intérprete de comandos Bash el ancho y alto de la terminal se obtiene con las variables de entorno $COLUMNS y $LINES respectivamente. Pero también se puede obtener la misma información con el comando tput. Para obtener esta información desde un programa Java basta con ejecutar un proceso del sistema, obtener la salida de estos comandos y procesarla para obtener la información.

1
2
3
4
5
6
$ echo $COLUMNS $LINES
80 24
$ tput cols
80
$ tput lines
24

El siguiente ejemplo muestra varias barras de progreso utilizando la secuencia de escape \33[{COUNT}B, \33[{COUNT}A para posicionar el cursor una linea abajo o arriba y la información de ancho y alto de la terminal obtenida de ejecutar como un subproceso el comando tput.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package io.github.picodotdev.blogbitix.javaterminal;

import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) throws Exception {
        Terminal terminal = new Terminal();
        Printer printer = new Printer(terminal);

        List<Progress> progresses = new ArrayList<>();
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 5; ++i) {
            Progress progress = new Progress(printer, i, i * 5 + 5);
            progresses.add(progress);
        }

        for (Progress progress : progresses) {
            Thread thread = new Thread(progress);
            thread.start();
            threads.add(thread);
        }

        for (Thread thread: threads) {
            thread.join();
        }

        System.out.println();
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package io.github.picodotdev.blogbitix.javaterminal;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.Charset;

public class Terminal {

    private int width;
    private int heigth;

    public int getWidth() {
        return width;
    }

    public int getHeigth() {
        return heigth;
    }

    synchronized void refresh() {
        try {
            Process colsProcess = new ProcessBuilder("bash", "-c", "tput cols 2> /dev/tty").start();
            Process linesProcess = new ProcessBuilder("bash", "-c", "tput cols 2> /dev/tty").start();
            BufferedReader colsReader = new BufferedReader(new InputStreamReader(colsProcess.getInputStream(), Charset.forName("utf-8")));
            BufferedReader linesReader = new BufferedReader(new InputStreamReader(linesProcess.getInputStream(), Charset.forName("utf-8")));
            String cols = colsReader.readLine();
            String lines = linesReader.readLine();

            width = Integer.parseInt(cols);
            heigth = Integer.parseInt(lines);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package io.github.picodotdev.blogbitix.javaterminal;

public class Printer {

    private Terminal terminal;
    private int line;

    public Printer(Terminal terminal) {
        this.terminal = terminal;
        this.line = 0;
    }

    public Terminal getTerminal() {
        return terminal;
    }

    synchronized void print(String text, int line) {
        setLine(line);
        erase();
        System.out.print(text);
    }

    void refresh() {
        terminal.refresh();
    }

    private void setLine(int line) {
        String command = "";
        if (this.line < line) {
            command = "B";
        } else if (this.line > line) {
            command = "A";
        }
        if (!command.equals("")) {
            System.out.print(String.format("\033[%s%s", Math.abs(this.line - line), command));
            this.line = line;
        }
    }

    private void erase() {
        System.out.print("0f\33[2K\r");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package io.github.picodotdev.blogbitix.javaterminal;

import java.time.LocalDateTime;
import java.time.ZoneOffset;

public class Progress implements Runnable {

    private Printer printer;
    private int line;
    private int seconds;

    public Progress(Printer printer, int line, int seconds) {
        this.printer = printer;
        this.line = line;
        this.seconds = seconds;
    }

    public void run() {
        long duration = seconds * 1000;
        long start = System.currentTimeMillis();
        long now = start;
        while (start + duration > now) {
            printer.refresh();

            now = System.currentTimeMillis();
            long percent = Math.min(100, Math.round(((double) (now - start) / duration) * 100));
            int size = printer.getTerminal().getWidth() - 11 - 8 - 2;
            long characters = Math.round((double) (size * percent / 100));
            char[] chars = new char[size];
            for (int i = 0; i < chars.length; ++i) {
                chars[i] = (i < characters) ? '#' : '-';
            }

            String nameStatus = "jdk-openjdk";
            String progressStatus = String.valueOf(chars);
            String percentStatus = String.valueOf(percent) + "%";

            String status = String.format("%-11s [%s] %s", nameStatus, progressStatus, percentStatus);
            printer.print(status, line);
            sleep();
        }
    }

    private void sleep() {
        try {
            Thread.sleep(500);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
Progreso escribiendo en la misma línea de la consola

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew build && ./run.sh.