Haskell -- brick を使ってみる

Haskell でつくられたゲームが

Haskell GameDev –

に載ってゐた

それぞれ 色々と面白いのだが

Swarm – Haskell GameDev

のやうな ターミナルアプリに魅かれ
これに使はれてゐる brick といふライブラリを試してみたくなった

github.com

ドキュメント類も色々充實してゐる感じなのだが 素直にインストラクションに従はず
デモを なんとなく 試してみることにした

ちなみに stack はインストールしてある前提で話を進める
(はじめてHaskellをインストールする場合 今では GHCup がおすすめだと思ってゐる)

stack new bdemo

などとして bdemo といふ名のディレクトリをつくったら
中の app ディレクトリ内にある Main.hs の中身を デモプログラムに置き換へる
あと bdemo ディレクトリ内の package.yaml ファイル に いくつかパッケージを追加する

dependencies:
 - base >= 4.7 && < 5
 - brick
 - microlens
 - microlens-th
 - microlens-mtl
 - vty

base >= 4.7 && < 5 といふのは 最初から書かれてゐるやつだ
その他は書いておかないと エラーになる

あとは

stack run

として ビルド 實行すればよい(すごい色々インストールされるから時間かかるけど)
今回使った デモコードは EditDemo.hs といふやつで

EditDemo
みたいな實行画面になる
さて 中身を見てみやうか

全體を一度載せる(まだ何だか分からん)

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE RankNTypes #-}
module Main where

import Lens.Micro
import Lens.Micro.TH
import Lens.Micro.Mtl
import qualified Graphics.Vty as V

import qualified Brick.Main as M
import qualified Brick.Types as T
import Brick.Widgets.Core
  ( (<+>)
  , (<=>)
  , hLimit
  , vLimit
  , str
  )
import qualified Brick.Widgets.Center as C
import qualified Brick.Widgets.Edit as E
import qualified Brick.AttrMap as A
import qualified Brick.Focus as F
import Brick.Util (on)

data Name = Edit1
          | Edit2
          deriving (Ord, Show, Eq)

data St =
    St { _focusRing :: F.FocusRing Name
       , _edit1 :: E.Editor String Name
       , _edit2 :: E.Editor String Name
       }

makeLenses ''St

drawUI :: St -> [T.Widget Name]
drawUI st = [ui]
    where
        e1 = F.withFocusRing (st^.focusRing) (E.renderEditor (str . unlines)) (st^.edit1)
        e2 = F.withFocusRing (st^.focusRing) (E.renderEditor (str . unlines)) (st^.edit2)

        ui = C.center $
            (str "Input 1 (unlimited): " <+> (hLimit 30 $ vLimit 5 e1)) <=>
            str " " <=>
            (str "Input 2 (limited to 2 lines): " <+> (hLimit 30 e2)) <=>
            str " " <=>
            str "Press Tab to switch between editors, Esc to quit."

appEvent :: T.BrickEvent Name e -> T.EventM Name St ()
appEvent (T.VtyEvent (V.EvKey V.KEsc [])) =
    M.halt
appEvent (T.VtyEvent (V.EvKey (V.KChar '\t') [])) =
    focusRing %= F.focusNext
appEvent (T.VtyEvent (V.EvKey V.KBackTab [])) =
    focusRing %= F.focusPrev
appEvent ev = do
    r <- use focusRing
    case F.focusGetCurrent r of
      Just Edit1 -> zoom edit1 $ E.handleEditorEvent ev
      Just Edit2 -> zoom edit2 $ E.handleEditorEvent ev
      Nothing -> return ()

initialState :: St
initialState =
    St (F.focusRing [Edit1, Edit2])
       (E.editor Edit1 Nothing "")
       (E.editor Edit2 (Just 2) "")

theMap :: A.AttrMap
theMap = A.attrMap V.defAttr
    [ (E.editAttr,                   V.white `on` V.blue)
    , (E.editFocusedAttr,            V.black `on` V.yellow)
    ]

appCursor :: St -> [T.CursorLocation Name] -> Maybe (T.CursorLocation Name)
appCursor = F.focusRingCursor (^.focusRing)

theApp :: M.App St e Name
theApp =
    M.App { M.appDraw = drawUI
          , M.appChooseCursor = appCursor
          , M.appHandleEvent = appEvent
          , M.appStartEvent = return ()
          , M.appAttrMap = const theMap
          }

main :: IO ()
main = do
    st <- M.defaultMain theApp initialState
    putStrLn "In input 1 you entered:\n"
    putStrLn $ unlines $ E.getEditContents $ st^.edit1
    putStrLn "In input 2 you entered:\n"
    putStrLn $ unlines $ E.getEditContents $ st^.edit2

はい わかりません
では まづ 最初の import文などは讀み飛ばして

data Name = Edit1
          | Edit2
          deriving (Ord, Show, Eq)

data St =
    St { _focusRing :: F.FocusRing Name
       , _edit1 :: E.Editor String Name
       , _edit2 :: E.Editor String Name
       }

あたりから見ていくか
Name は 單に名前のことだと分かる
St は状態なんだらうけど focusRing って何だろ?
edit1 とか edit2 は デモ画面にある 二つのエディタのことを指すんだらう

次の makeLenses ''St みたいなところは
TemplateHaskell といふ 言語拡張を使って 何かつくってるんだ みたいな理解をしてゐる
だって St の左に チョンチョンって ふたつついてるでしょ?
これが TemplateHaskell の特徴なんだよ きっと

drawUI :: St -> [T.Widget Name]
drawUI st = [ui]
    where
        e1 = F.withFocusRing (st^.focusRing) (E.renderEditor (str . unlines)) (st^.edit1)
        e2 = F.withFocusRing (st^.focusRing) (E.renderEditor (str . unlines)) (st^.edit2)

        ui = C.center $
            (str "Input 1 (unlimited): " <+> (hLimit 30 $ vLimit 5 e1)) <=>
            str " " <=>
            (str "Input 2 (limited to 2 lines): " <+> (hLimit 30 e2)) <=>
            str " " <=>
            str "Press Tab to switch between editors, Esc to quit."

drawUI ってからには 描画に関連してるんだらう
あっ! FocusRing でてきた
e1とe2 は エディタのことだらう
それに FocusRing がある ってことか?
ui のところは まさに Demo画面に描かれてゐる Input 1 とか Input 2 のことだな
色々位置などを設定してゐるのだらう
e1 とかで定義されてゐる

F.withFocusRing (st^.focusRing) (E.renderEditor (str . unlines)) (st^.edit1)

をちょと 見ていかう
F は インポート文の

import qualified Brick.Focus as F

で指定されてゐるやうに Brick.Focus のことだ
その中の withFocusRing といふ函數を使用してゐる
これを Hackage で確認しやう

withFocusRing :: (Eq n, Named a n) => FocusRing n -> (Bool -> a -> b) -> a -> b

となってる
FocusRing n といふのが フォーカスリング(多分 エディットする部分が黄色くなるやうなやつだと思ふ) (Bool -> a -> b) といふのが 真偽値と なんかを取って なんかを返す函數 (E.renderEditor (str .unlines) の部分)
a にあたるのが (st^.edit1)の部分

あぶないあぶない
ちょっと 深く見すぎた
次行ってみやう

appEvent :: T.BrickEvent Name e -> T.EventM Name St ()
appEvent (T.VtyEvent (V.EvKey V.KEsc [])) =
    M.halt
appEvent (T.VtyEvent (V.EvKey (V.KChar '\t') [])) =
    focusRing %= F.focusNext
appEvent (T.VtyEvent (V.EvKey V.KBackTab [])) =
    focusRing %= F.focusPrev
appEvent ev = do
    r <- use focusRing
    case F.focusGetCurrent r of
      Just Edit1 -> zoom edit1 $ E.handleEditorEvent ev
      Just Edit2 -> zoom edit2 $ E.handleEditorEvent ev
      Nothing -> return ()

最初の行は 型注釈の部分 次からの appEvent 三つは 何か對應するキーを押すと 何かする ってやつだらう
focusNext とか 分かりやすい名前が付いてゐる
最後は 何か focusGetCurrent といふものの値によって 場合分けしてるな
今 ここに フォーカスしてゐるなら これをしろ みたいなことを 言っているんだらう・・・

initialState :: St
initialState =
    St (F.focusRing [Edit1, Edit2])
       (E.editor Edit1 Nothing "")
       (E.editor Edit2 (Just 2) "")

theMap :: A.AttrMap
theMap = A.attrMap V.defAttr
    [ (E.editAttr,                   V.white `on` V.blue)
    , (E.editFocusedAttr,            V.black `on` V.yellow)
    ]

最初の状態を定義してゐるのは明らかだ
Edit1 Nothing
Edit 2 (Just 2)
とある
Nothing は 何行書いてもスクロールするエディタにするよ ってことだらう
Just 2 は 2行までに限定する といふことだ

あと theMap では 明らかに 白 青 黄色 といふ指定をしてゐるのがわかる
といふことは これが フォーカスされてゐるときと されてゐないときの色についてのデータだ!

appCursor :: St -> [T.CursorLocation Name] -> Maybe (T.CursorLocation Name)
appCursor = F.focusRingCursor (^.focusRing)

なんだらう これは
カーソルについての指定さらうか
フォーカスリング カーソルといふのが あるんだらう (たぶん)

theApp :: M.App St e Name
theApp =
    M.App { M.appDraw = drawUI
          , M.appChooseCursor = appCursor
          , M.appHandleEvent = appEvent
          , M.appStartEvent = return ()
          , M.appAttrMap = const theMap
          }

アプリ全体の定義だらう
描画は drawUI で
イベント処理は appEvent で スタートイベントは 特に指定がない といふことだらうか
あと 先程指定されてゐた フォーカス色の theMap が入ってゐる

main :: IO ()
main = do
    st <- M.defaultMain theApp initialState
    putStrLn "In input 1 you entered:\n"
    putStrLn $ unlines $ E.getEditContents $ st^.edit1
    putStrLn "In input 2 you entered:\n"
    putStrLn $ unlines $ E.getEditContents $ st^.edit2

メイン函數
これが アプリを實行すると 最初に實行される
do 以下の一行目が Demo画面を表示させてゐる部分だ
Escキー を押すと エディタに入れてゐた情報を ターミナルに表示して終了するから
二行目以下は その部分だらう

うーん
まだちゃんと理解はできてゐないけど
Input2 の方にコマンドを打って リアルタイムに Input1 にログを表示させるのに向いてゐると思ふ
ちょうど 今 やってゐる kdbtl で さういふことをしたいと思ってゐたので これがそのまま使へるかも!!?
あとは タイマーイベントの部分を どう書くか 調べてみたい
とりあへず 今は こんな感じで