Contattaci

Creare applicazioni console complesse (e belle) in C# con Spectre.Console

spectreconsole
  • Data: 25 Ottobre 2022
  • Autore: Federico Teotini
  • Categorie

  • Giuneco Tech

    Il nostro settore richiede uno studio continuo e una forte attenzione all’innovazione. Incentiviamo quindi dei programmi formativi annuali per tutti i componenti del team, con ore dedicate (durante l’orario di lavoro) e serate formative sia online che in presenza. Sponsorizziamo eventi, sia come partner che semplicemente come partecipanti, e scriviamo articoli su quello che abbiamo imparato per essere, a nostra volta, dei divulgatori.

    Vai alla sezione Team
  • Come developer, spesso ci troviamo a interagire con il terminal e con le varie applicazioni pensate per essere sfruttate via CLI. Per alcuni è un piacere, per altri è un incubo, ma a qualsiasi categoria apparteniate, nessuno può sopportare applicazioni console che non abbiano un’interfaccia (grafica e funzionale) chiara. Se state pensando a git avete perfettamente capito ciò a cui mi riferisco…
    In questo articolo scopriremo come creare applicazioni console che siano belle, funzionali e che si integrino perfettamente con tutto l’ecosistema dotnet.
    Vi presento Spectre.Console!

    Spectre.Console

    Spectre.Console è un pacchetto NuGet che permette di creare delle vere e proprie applicazioni invocabili da terminale, complete di tutto ciò che possiate immaginare utile, chiavi in mano!

     

    spectre

     

    Questa libreria open source è formata da due progetti complementari fra loro:
    Spectre.Console che mette a disposizione tutta una serie di widgets (tabelle, alberi, grafici…) e di utilities per stilizzare l’aspetto grafico
    Spectre.Cli ovvero l’infrastruttura che permette di progettare l’esperienza di utilizzo complessiva dell’applicazione (args parsing e la loro specifica, alberatura dei comandi…). Questo scheletro è pensato anche per integrarsi con i vari framework di logging e di dependency injection, offrendo, quindi, la massima flessibilità.

     

    Impariamo a conoscere questo progetto creando insieme una semplicissima applicazione di esempio.

    Cosa andremo a costruire

    Per iniziare a intravedere le potenzialità di Spectre.Console, costruiremo una semplice console application che mostri il meteo (si, lo so, poca fantasia) per una data località.

    app meteo

    Scaffold

    Creiamo una console application e installiamo un po’ di dipendenze:

    Apriamo il .csproj e andiamo a modificarlo

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <!-- new project! why not? -->
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <WarningsAsErrors>nullable</WarningsAsErrors>
        <LangVersion>latest</LangVersion> 
      </PropertyGroup>
    
      <!-- ... -->
    
    </Project>

    Scheletro del primo Command

    Il primo e più importante Command che andremo a scrivere è, appunto, quello che fornirà il meteo.
    Creiamo un file Commands/ForecastCommand.cs

    Abbiamo definito un command con i suoi Settings. In generale, i parametri di un’applicazione console (prendiamo ad esempio dotnet new) sono di due tipi:
    1. Options – ovvero parametri il cui valore è preceduto da un identificatore, ad esempio dotnet new –output [valore] oppure con uno shorthand dotnet new -o [valore]
    2. Arguments – parametri che sono specificati senza identificatore, ad esempio dotnet new [valore]
    Nel nostro ForecastCommand abbiamo definito un solo parametro di tipo Argument tramite l’attributo [CommandArgument(0, “<location>”)] che specifica:
    – La posizione 0, ovvero è il primo Argument
    – Il nome, ovvero ‘location’
    – L’obbligatorietà, perché il nome è racchiuso fra ‘<>’
    Abbiamo, inoltre, aggiunto una descrizione che comparirà nell’help della nostra applicazione.

    Boilerplate

    Per integrare Spectre.Cli con il ServicesContainer di Microsoft, è necessario scrivere un po’ di boilerplate.
    Creiamo una cartella Infrastructure e aggiungiamoci

    Vogliamo, inoltre, poterci interfacciare con il GenericHost sempre di Microsoft; creiamo, quindi, qualche extension method sempre in Infrastructure.

    Collegare i fili

    Siamo ora pronti a scrivere il nostro Program.cs mettendo insieme i vari pezzi!

    Lanciando l’applicazione con dotnet run — Firenze, Spectre eseguirà il ForecastCommand (al momento non implementato):
    Error: The method or operation is not implemented.
    Se non passiamo l’Argument ‘Firenze’, chiamando semplicemente dotnet run, otteniamo correttamente un errore:
    Error: Missing required argument 'location'.
    Spectre ha creato anche l’help per noi, invocabile con dotnet run — -h
    USAGE:
        wth <location> [OPTIONS]
    
    ARGUMENTS:
        <location>    The place where you want to know the forecast for
    
    OPTIONS:
        -h, --help    Prints help information

    Chiamare le Api per il meteo

    Il meteo verrà fornito dal servizio OpenWeatherMap. Registratevi e create una Api key.
    Dopodiché, aggiungete il pacchetto Refit:
    dotnet add package Refit.HttpClientFactory
    e aggiornate il Program.cs:

    Implementare ForecastCommand

    Iniziamo modificando ForecastCommand come segue:

    Vediamo come sia attiva la Dependency Injection grazie al glue code scritto in precedenza. Vediamo anche Spectre.Console all’opera tramite la classe statica AnsiConsole usata per scrivere sulla console i Widgets che andiamo ad implementare.

     Title widget

    Vogliamo creare una cosiddetta Rule che mostri il titolo e la località per cui stiamo richiedendo il meteo.
    Titolo app meteo
    Implementiamo il metodo Title():

    Iniziamo creando una Rule e dandogli un titolo: usiamo una speciale sintassi per specificare lo stile di questo testo; in pratica, lo stile scritto fra parentesi quadre, ad esempio [orange1 underline], viene applicato fino a quando questo viene chiuso con [/].
    Infine con un Api fluent, andiamo a specificare anche lo stile della Rule stessa, in questo caso il colore e la posizione del titolo all’interno della Rule.

    ForecastPanel widget

    Creiamo ora un Panel, che non è altro che un contenitore per altri Widgets.
    previsioni
    Implementiamo il metodo CreateForecastPanel():

    In pratica stiamo creando un Panel a cui stiamo applicando:
    – un contenuto, che in questo caso è un widget Table
    – un header specificandone il testo, lo stile tramite la sintassi che abbiamo introdotto precedentemente e la sua posizione
    – dei bordi stondati con relativo colore
    Il contenuto informativo è, quindi, rappresentato da una tabella con una sola colonna:

    Anche in questo caso specifichiamo lo stile della tabella e poi la costruiamo in modo dichiarativo, usando anche qui la sintassi [] per lo stile del testo.

    Aggiungere un secondo command

    A questo punto la nostra applicazione console di esempio è completa e funzionante. Tuttavia, al momento, la vostra personale Api key di OpenWeatherMap è scritta in chiaro all’interno di ForecastCommand. Possiamo sfruttare questa “falla” per mostrare come aggiungere un ulteriore command all’applicazione.
    Per prima cosa creiamo un servizio che si occupi di salvare la chiave su un file e, successivamente, di recuperarla.

    Questo servizio è banale e assolutamente non adatto per essere usato in un’applicazione reale, ma per il risultato che vogliamo ottenere è più che sufficiente.
    Creiamo AddKeyCommand.cs sotto la cartella Commands, che non farà altro che prendere in input la chiave e chiamare il servizio di salvataggio su file:

    Registriamo il command nel Program.cs:

    Richiamando l’help con dotnet run — -h, possiamo vedere come il nuovo command sia stato correttamente registrato:
    USAGE:
        wth <location> [OPTIONS]
    ARGUMENTS:
        <location>    The place where do you want to know the forecast                                                              
    OPTIONS:
        -h, --help    Prints help information
    COMMANDS:
        add-key <ApiKey>    Save your personal OpenWeatherMap ApiKey
    ed è quindi possibile invocarlo con dotnet run — add-key “LA_VOSTRA_API_KEY”.

     

    Salvata la vostra chiave, passiamo ad aggiornare ForecastCommand in modo che la recuperi dal file:

    Dotnet tool

    Finora abbiamo invocato la nostra applicazione console tramite dotnet run perché eravamo in fase di sviluppo. Possiamo trasformare la nostra applicazione console in un dotnet tool che ci permetterà di:
    – interagire con l’applicazione usando un nome custom, nel nostro esempio wth
    – pacchettizzarla ed eventualmente distribuirla tramite NuGet
    – installarla e quindi averla disponibile nelle nostre shell

     

    Iniziamo con l’aggiungere queste poche direttive al .csproj:
    <PropertyGroup>
      <PackAsTool>true</PackAsTool>
      <ToolCommandName>wth</ToolCommandName>
      <PackageOutputPath>./nupkg</PackageOutputPath>
    </PropertyGroup>
    e concludiamo creando il pacchetto con
    dotnet pack -c Release
    il cui risultato sarà il file ./nupkg/SpectreExample.Weather.1.0.0.nupkg.

     

    In caso volessimo distribuire la nostra applicazione pubblicamente, la potremmo caricare su https://www.nuget.org e quindi sarebbe disponibile per essere installata usando il comando dotnet tool install. Per questo esempio, installeremo il tool direttamente dalla cartella locale ./nupkg senza bisogno di caricare niente su NuGet.

    Installazione

    dotnet tool install --global --add-source ./nupkg SpectreExample.Weather
    Il parametro –global indica di installare la nostra applicazione come tool globale, ovvero lo aggiunge alla variabile di ambiente PATH.
    Il parametro –add-source, invece, indica al CLI di dotnet di aggiungere temporaneamente la cartella ./nupkg come sorgente locale alla lista di quelle di NuGet.

     

    L’output ci mostra sia il nome con cui possiamo invocare il tool (ovvero wth), sia la versione installata:
    You can invoke the tool using the following command: wth
    
    Tool 'spectreexample.weather' (version '1.0.0') was successfully installed.

     

    Siamo finalmente pronti a usare la nostra applicazione meteo come una vera CLI application!
    wth Londra

    Conclusioni; potenzialità estetiche e di infrastruttura

    Costruendo un esempio, abbiamo visto le potenzialità della libreria Spectre.Console sia in termini di widget e stile sia in termini di infrastruttura. La semplicità della sintassi per applicare lo stile, la quantità di widget disponibili, l’esperienza d’uso pensata nei minimi dettagli, ma soprattutto la completezza di tutte le funzionalità offerte dalla libreria ne fanno un coltellino svizzero da portare sempre nel nostro zaino da sviluppatori.

     

    Cerchiamo di dare un po’ d’amore alle spesso bistrattate applicazioni console …e non provate mai a scrivere un tool con interfaccia ispirata a git!

    Appendice: esempio di disinstallazione

    Cancellate la cartella .wth presente nella cartella del vostro profilo ed eseguite
    dotnet tool uninstall --global SpectreExample.Weather