Página de Guillaume Hoffmann

View on GitHub

% Programación Declarativa: Libería Brick

Brick es una librería Haskell que permite hacer interfaces de usuario en modo texto.

Instalación

cabal install brick

o

stack install brick

Interfaz sin estado

Ejemplo ReadmeDemo.hs : una interfaz no-interactiva

module Main where

import Brick
import Brick.Widgets.Center
import Brick.Widgets.Border
import Brick.Widgets.Border.Style

ui :: Widget ()
ui =
    withBorderStyle unicode $
    borderWithLabel (str "Hello!") $
    (center (str "Left") <+> vBorder <+> center (str "Right"))

main :: IO ()
main = simpleMain ui

Compilar y ejecutar con:

ghc ReadmeDemo.hs -package brick  -threaded
./ReadmeDemo

Explicación (es útil buscar en Hoogle también):

Otras funciones y operadores que podés probar en este programa:

Interfaz para una aplicación con estado

El tipo App y defaultMain

El tipo App es un tipo registro que provee varias funciones:

data App s e n =
    App { appDraw         :: s -> [Widget n]
        , appChooseCursor :: s -> [CursorLocation n] -> Maybe (CursorLocation n)
        , appHandleEvent  :: s -> BrickEvent n e -> EventM n (Next s)
        , appStartEvent   :: s -> EventM n s
        , appAttrMap      :: s -> AttrMap
        }

El tipo App está parametrizado sobre tres tipos. Estas variables de tipo aparecen en los protótipos de funciones y tipos de la librería brick. Son:

Ejecutar una aplicación

Para ejecutar una App, la pasamos a Brick.Main.defaultMain o Brick.Main.customMain junta con el valor inicial del estado de la aplicación:

main :: IO ()
main = do
  let app = App { ... }
      initialState = ...
  finalState <- defaultMain app initialState
  -- Use finalState and exit

La función customMain es para usos más avanzados.

appDraw: dibujar una interfaz

El valor de appDraw es una función que convierte el estado actual de la aplicación en una lista de capas de tipo Widget, listadas desde la más arriba, que constituyen la intefaz. Cada Widget se convierte en una capa vty y las capas resultantes se dibujan en la terminal.

El tipo Widget es el tipo de las instrucciones de dibujo, y usualmente un Widget se construye combinando varios Widget. Estas instrucciones se ejecutan tomando en cuenta 3 cosas:

La función appDraw es llamada cuando el bucle de evento empieza a dibujar la aplicación cuando aparece por primera vez. También es llamada justo después de que se procesa un evento por appHandleEvent.

Las funciones de dibujo se encuentran en Brick.Widgets.Core, y existen otros módulos Brick.Widgets para usos más específicos.

appHandleEvent: manejar eventos

appHandleEvent es una función que decide como modificar el estado de la aplicación como resultado de un evento:

appHandleEvent :: s -> BrickEvent n e -> EventM n (Next s)

El parámetro de tipo s es el estado de la aplicación cuando ocurre el evento. appHandleEvent decide cómo cambiar el estado según el evento y luego lo devuelve.

El segundo parámetro de tipo BrickEvent n e es el evento en sí. Las variables de tipo n y e corresponden al tipo del nombre de recurso y tipo de eventos de su aplicación, respectivamente, y deben corresponderse con los tipos en App y EventM.

El tipo del valor de devolución Next s describe lo que debería pasar kuego de que el gestor de eventos termina. Tenemos tres opciones:

La mónada EventM sirve para manejar los eventos. Esta mónada es un transformador alrededor de IO entonces podés hacer acciones I/O en esta mónada usando la función liftIO. Tomá en cuenta que el tiempo pasado en tu gestor de eventos es tiempo durante el cual la interfaz de usuario no responde, entoces tomalo en cuenta cuando decidís si vas a tener hilos para hacer algún trabajo en lugar de hacer el trabajo en el gestor de eventos.

Arrancar: appStartEvent

Cuando empieza una aplicación, puede ser útil hacer algunas acciones típicamente solo posibles cuando un evento ocurrió, por ejemplo inicializar algún estado de ventana desfilable. Dado que acciones como esta solo se pueden llevar a cabo en un EventM y dado que no queremos esperar hasta que ocurra el primer evento para hacer ese trabajo en appHandleEvent, el tipo App provee la función appStartEvent con este propósito:

appStartEvent :: s -> EventM n s

Esta función toma el estado inicial y lo devuelve dentro de un EventM, posiblemente haciendo pedidos de ventanas desfilables. Esta función es llamada una y solamente una vez, cuando empieza la aplicación. En general, querés simplemente definirla como return para la mayoría de las aplicaciones.

Ubicar el cursor: appChooseCursor

El proceso de renderización de un Widget puede devolver información sobre donde ese widget quiere ubicar el cursor. Por ejemplo, un editor de texto necesita indicar la posición del cursor. Sin embargo, dado que un Widget puede ser constituido de varios widgets que ubican el cursor, debemos tener una manera de elegir cuál de las posiciones de cursor reportadas, si hay, es la que queremos reconocer.

Para elegir cual ubicación de cursor usar, o no decidir de imprimir ninguno, definimos la función:

appChooseCursor :: s -> [CursorLocation n] -> Maybe (CursorLocation n)

El bucle de eventos renderiza la intefaz y colecta los valores de Brick.Types.CursorLocation producidos por el proceso de renderización y pasa estos valores, juntos con el estado de la aplicación, a esta función. Usando el estado de la aplicación (por ejemplo para saber qué campo de formulario de texto está “enfocado”) podés decidir cuál de las ubicaciones devolver, o devolver Nothing si no querés mostrar un cursor.

Documentación y ejemplos

Más documentación

Consulta la guía oficial (en inglés) de Brick.

Algunas fuentes interesantes