Introduzione alla libreria Jansi

Questo articolo fornisce una introduzione all’utilizzo della libreria Jansi per Java che permette di scrivere testo su console con colori/attributi sfruttando le ben note “sequenze di escape ANSI”.

il
Senior Java developer, Collaboratore di IProgrammatori

Chi utilizza da un po’ di tempo i sistemi di build Maven e/o Gradle, dovrebbe aver notato che quando si lancia il build da una console (attenzione: si intende la “vera” console/terminale di un Sistema Operativo, non quella di un IDE es. Eclipse!) l’output risulta “colorato”, ovvero il testo ha varie parti di colore differente. Un esempio di output lo si può vedere nel seguente screenshot (è il build Maven di un mio progetto personale):

Screenshot build Maven

Il testo colorato viene prodotto in output da Maven grazie all’utilizzo di una libreria Java specifica che si chiama Jansi. Si tratta di una piccola libreria open-source (licenza Apache Software 2.0) che permette di gestire facilmente le sequenze di caratteri che si chiamano ANSI escape sequences. Queste sequenze sono ben note da tantissimi anni nell’ambito dei terminali testuali perché permettono di controllare il terminale impostando la posizione del cursore, i colori e attributi del testo scritto e altro. Si può reperire varia documentazione su queste sequenze, ad esempio partendo dalla pagina di Wikipedia ANSI escape code.

Queste sequenze hanno la particolarità di iniziare con il carattere ESC (escape) che nello standard ASCII ha il codice decimale 27 (1B in esadecimale e 033 in ottale). Il carattere ESC è un carattere di controllo nello standard ASCII, quindi non è di per sé un carattere grafico stampabile ma dipende anche da dove lo si visualizza (es. su un word processor piuttosto che su una console) e con quale font di carattere.

In queste sequenze ANSI, dopo il carattere ESC ci può essere un numero variabile di altri caratteri normali, secondo delle specifiche ben precise. Se ad esempio su una console/terminale che supporta queste sequenze si vuole partire a scrivere il testo in colore rosso, la sequenza da scrivere in output è ESC[31m .

La principale difficoltà nell’utilizzo di queste sequenze ANSI all’interno delle applicazioni “console” (indipendentemente dal linguaggio usato per l’applicazione) sta nel fatto che il supporto di queste sequenze purtroppo non è affatto uniforme, né tanto meno “standard” su tutti i vari Sistemi Operativi esistenti. Nei sistemi Unix-like il supporto generalmente è (quasi) sempre garantito, salvo casi particolari di applicazioni console o emulatori di terminale che non le supportano. Su altri SO il supporto può non essere presente o può essere presente solo in determinate versioni del SO oppure è determinato in base ad altri fattori (configurazioni, driver, ecc...). La pagina linkata prima su Wikipedia spiega sicuramente meglio tutte queste questioni che sfortunatamente non sono affatto banali.

Il vero problema pertanto si ha quando in una applicazione si vuole emettere in output queste sequenze. Prendiamo ad esempio il seguente programma Java che è davvero basilare:

public class ProvaColore {
    public static void main(String[] args) {
        System.out.println("\033[31mCIAO\033[0m");
    }
}

Il println emette in particolare due sequenze di escape ANSI: ESC[31m per attivare il foreground rosso e ESC[0m per resettare i colori. Da notare che in Java per rappresentare il codice di escape nelle stringhe “letterali” si può usare la sequenza di escape ottale \033 oppure la sequenza di escape Unicode \u001b (oppure \u001B).

Provando ad eseguire questo programma da una console/terminale del SO, ci possono essere due risultati differenti:

  1. Se la console/terminale supporta le sequenze ANSI, si vede la scritta CIAO scritta così in rosso
  2. Se la console/terminale non supporta le sequenze ANSI, si vedono letteralmente tutti i caratteri dei codici, tipo: ←[31mCIAO←[0m

Questa chiaramente non è una buona cosa, perché nel secondo caso l’output risulta “sballato” e oltretutto poco leggibile. Purtroppo cercare di riconoscere il supporto o meno di queste sequenze e poi emettere o non emettere in output le sequenze di escape richiederebbe molto codice e parecchia logica, che sarebbe non banale da realizzare e oltretutto si dovrebbe sostanzialmente replicare in tutte le applicazioni console. La libreria Jansi fortunatamente si fa carico praticamente di tutti questi aspetti.

Utilizzo di Jansi

Innanzitutto i riferimenti online per questa libreria sono i seguenti: fusesource.github.io/jansi/ (home page) e github.com/fusesource/jansi (progetto su GitHub). Nella pagina raggiungibile dal primo link si trovano anche i link alla documentazione javadoc ufficiale (da consultare assolutamente durante lo sviluppo con Jansi!) e alla pagina dei Download da cui si può scaricare il jar della libreria.

La libreria Jansi è estremamente entrocontenuta grazie alle seguenti caratteristiche:

  • consiste in un singolo file jar di dimensioni abbastanza ridotte (poco più di 200 KByte nell’ultima versione disponibile al momento)
  • non ha a sua volta altre dipendenze (non richiede ulteriori jar per il corretto funzionamento)
  • contiene già all’interno del jar le librerie “native” (es. .dll/.so ecc... per i sistemi operativi Windows, Linux, OS X e FreeBSD) che sono necessarie per il funzionamento della libreria e che Jansi estrae e carica in maniera autonoma

Per poter utilizzare materialmente la libreria Jansi ci sono diverse possibilità. Si può scaricare il jar dal sito del progetto (pagina dei Download indicata prima) oppure se si usa uno strumento di build come Maven/Gradle si può ottenere l’artifact usando le seguenti coordinate:

Maven:

<dependency>
    <groupId>org.fusesource.jansi</groupId>
    <artifactId>jansi</artifactId>
    <version>2.3.2</version>
</dependency>

Gradle (Groovy DSL):

implementation 'org.fusesource.jansi:jansi:2.3.2'

Per altri dettagli e per vedere tutte le versioni disponibili anche più recenti, si può consultare il Maven Central: search.maven.org/artifact/org.fusesource.jansi/jansi

Come già detto nella precedente sezione, il supporto alle sequenze di escape ANSI è molto vario e dipende in gran parte dal Sistema Operativo. La libreria Jansi si preoccupa di verificare automaticamente se c’è il supporto alle sequenze di escape ANSI e inoltre fornisce a sua volta una ottima “astrazione” che ci permette in sostanza di ignorare tutti quei dettagli che variano da un Sistema Operativo all’altro.

Con la libreria Jansi in pratica si opera nel seguente modo: all’inizio della applicazione si deve invocare il metodo AnsiConsole.systemInstall(). Questo metodo si occupa, tra altre cose, di “installare” due nuovi oggetti PrintStream per lo standard-output e lo standard-error, come è reso possibile dalla classe java.lang.System. Il supporto si può successivamente disattivare invocando il metodo complementare che si chiama systemUninstall().

Una volta invocato il systemInstall() è sempre possibile scrivere in output le sequenze di escape ANSI. I due PrintStream speciali di Jansi infatti contengono la logica di filtro/interpretazione di queste sequenze e in base al supporto o meno, può avvenire una delle seguenti cose:

  1. Se si sta scrivendo su una “vera” console/terminale e le sequenze di escape ANSI sono supportate, esse vengono emesse in output così come sono, ottenendo quindi gli effetti desiderati
  2. Se non si sta scrivendo su una “vera” console/terminale (perché ad esempio l’output viene redirezionato su un file) oppure le sequenze di escape ANSI non sono supportate, allora esse vengono filtrate e rimosse in modo da non causare problemi nel testo prodotto
  3. Se si sta scrivendo su una “vera” console/terminale e le sequenze di escape ANSI non sono supportate ma esiste una API “nativa” del SO per gestire la console/terminale a “basso” livello (è il caso di Windows), allora le sequenze di escape ANSI vengono interpretate e tradotte in chiamate alla API nativa del sistema per emulare il comportamento che le sequenze di escape ANSI dovrebbero produrre

Grazie a tutta questa logica che è già integrata in Jansi, non c’è bisogno di fare praticamente altro su questi aspetti. L’unica cosa che resta da fare è semplicemente scrivere le sequenze di escape ANSI. È certamente possibile scriverle come è stato mostrato prima, cioè banalmente con una stringa letterale es. "\033[31m" ma Jansi va oltre e mette a disposizione una API di più alto livello che semplifica la gestione dei colori e degli attributi.

Jansi offre due tecniche differenti per gestire colori e attributi: una fluent API e la classe AnsiRenderer.

La API “fluente” permette di sfruttare quello che viene chiamato method-chaining per poter fare invocazioni in sequenza come nel seguente modo:

System.out.println(ansi().fgRed().a("CIAO").reset());

Mentre con la classe AnsiRenderer (usabile direttamente o indirettamente) si può definire una stringa che contiene una sintassi specifica per descrivere i colori e gli attributi da usare, ad esempio:

System.out.println(ansi().render("@|red CIAO|@"));

In entrambi i casi, si ottiene una scritta CIAO in rosso (se supportato, ovviamente). Quel ansi() è un metodo statico che funge da factory per ottenere un oggetto di tipo org.fusesource.jansi.Ansi che è una sorta di buffer di caratteri (simile a StringBuffer, per intenderci) ma con in più i metodi per gestire colori e attributi. Tipicamente il metodo ansi() si importa nel sorgente tramite uno static import nel seguente modo:

import static org.fusesource.jansi.Ansi.ansi;

Esempio concreto di utilizzo della libreria Jansi

L’esempio pratico che propongo di seguito è una semplice classe Java con il classico metodo main che visualizza in maniera “colorata” alcune informazioni sul Sistema Operativo ottenute con la classe OperatingSystemMXBean. Non è particolamente importante conoscere benissimo questa classe, è sufficiente sapere che fa parte della API del management della piattaforma Java e possiede dei semplici metodi tipo getName(), getVersion() ecc... per ottenere queste informazioni.

import static org.fusesource.jansi.Ansi.ansi;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import org.fusesource.jansi.AnsiConsole;

public class OsInfoAnsi {
    public static void main(String[] args) {
        AnsiConsole.systemInstall();

        OperatingSystemMXBean osMxBean = ManagementFactory.getOperatingSystemMXBean();

        System.out.println();
        System.out.println(ansi().render("@|fg_yellow,bold,bg_blue Sistema Operativo:|@"));

        System.out.println(ansi().fgDefault().a("Nome ........... ").fgBrightCyan().a(osMxBean.getName()).reset());
        System.out.println(ansi().fgDefault().a("Versione ....... ").fgBrightBlue().a(osMxBean.getVersion()).reset());
        System.out.println(ansi().fgDefault().a("Architettura ... ").fgBrightRed().a(osMxBean.getArch()).reset());
        System.out.println(ansi().fgDefault().a("Processori ..... ").fgBrightGreen().a(osMxBean.getAvailableProcessors()).reset());

        AnsiConsole.systemUninstall();
    }
}

Il risultato si può vedere nel seguente screenshot:

Screenshot risultato esempio OsInfoAnsi