jtdaugherty / brick

A declarative Unix terminal UI library written in Haskell
Other
1.6k stars 164 forks source link

Help with events and state. Please :-) #411

Closed refaelsh closed 1 year ago

refaelsh commented 1 year ago

Here is an MRE I extracted from my actual code:

{-# LANGUAGE GADTs #-}

import Brick
import Brick.BChan
import Brick.Focus
import Brick.Widgets.Edit (Editor, editor, getCursorPosition, handleEditorEvent)
import Brick.Widgets.List
import Control.Monad
import Data.Vector (fromList)
import Graphics.Vty
import Numeric.Natural
import qualified View.TreeWidget as TreeWidget

main :: IO ()
main = do
  channel <- newBChan 100

  TreeWidget.initialize channel

  let buildVty = mkVty defaultConfig
  initialVty <- buildVty
  void
    $ customMain initialVty buildVty (Just channel) theApp
    $ State
      (list "some_name" (fromList [""]) 1)
      (focusRing ["some_other_name"])
    $ TerminatorState
      (editor "some_other_name" Nothing "something")
      0

data State where
  State ::
    { bricksList :: List String String,
      bricksFocusRing :: FocusRing String,
      terminatorState :: TerminatorState
    } ->
    State

data TerminatorState where
  TerminatorState ::
    { bricksEditor :: Editor String String,
      terminatorHistoryIndex :: Natural
    } ->
    TerminatorState

theApp :: App State TreeWidget.TreeRefreshEvent String
theApp =
  App
    { appDraw = const [str "some thing goes here"],
      appChooseCursor = showFirstCursor,
      appHandleEvent = handleEvents,
      appStartEvent = return (),
      appAttrMap = const $ attrMap (white `on` black) []
    }

handleEvents :: BrickEvent String TreeWidget.TreeRefreshEvent -> EventM String State ()
handleEvents e = do
  state <- get
  case e of
    (VtyEvent (EvKey KLeft [])) -> do
      let x = snd $ getCursorPosition $ bricksEditor $ terminatorState state
      if x < 3
        then nestEventM (bricksEditor (terminatorState state)) $ do return ()
        else nestEventM (terminatorState state) $ do
          handleEditorEvent (VtyEvent (EvKey KLeft []))
    _ -> return ()

Here is the error:

src/View/Main.hs|63 col 14 error| error:
||     • Couldn't match type ‘(Editor String String, ())’ with ‘()’
||       Expected: EventM String State ()
||         Actual: EventM String State (Editor String String, ())
||     • In the expression:
||         nestEventM (bricksEditor (terminatorState state)) $ do return ()
||       In a stmt of a 'do' block:
||         if x < 3 then
||             nestEventM (bricksEditor (terminatorState state)) $ do return ()
||         else
||             nestEventM (terminatorState state)
||               $ do handleEditorEvent (VtyEvent (EvKey KLeft []))
||       In the expression:
||         do let x = snd
||                      $ getCursorPosition $ bricksEditor $ terminatorState state
||            if x < 3 then
||                nestEventM (bricksEditor (terminatorState state)) $ do return ()
||            else
||                nestEventM (terminatorState state)
||                  $ do handleEditorEvent (VtyEvent (EvKey KLeft []))
||    |
|| 63 |         then nestEventM (bricksEditor (terminatorState state)) $ do return ()
||    |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Please help :-)

jtdaugherty commented 1 year ago

There are a couple of things going on here. First, the type error you reported is because the type of the then branch of the if does not match the required type of the overall function, handleEvents, which must evaluate to EventM String State (). To fix this, both branches of the if must have evaluate to that type. Right now, the branches currently have types EventM String State (Editor String String, ()) and EventM String State (TerminatorState, ()), respectively.

Here's how to think about nestEventM: it takes some value and allows you to work on that value as the state in the context of an EventM block. It then returns the possibly modified state as well as the EventM block's result. You're then responsible for putting the resulting modified state back where it belongs in your overall application state. (This may seem a bit involved, but if it does, that's the reason why the lens-based API exists. I know you said you don't want to use that API quite yet, and that's okay; but I wanted to point this out.)

Both of your if branches need to take care of storing the modified state they operate over. (As an aside, the then branch currently does nothing to the editor that it is given. Not sure if that's your intention.) In addition, the terminatorState use in the else branch will later fail to type-check because that state isn't the state that's needed to run handleEditorEvent. You'd need an Editor as the EventM state for that to work. Your then branch is closer to what you'd need to get that working.

A fix for the then branch would look something like:

if x < 3
    then do
        (newEditor, ()) <- nestEventM (bricksEditor (terminatorState state)) $ do
            handleEditorEvent someEvent
        modify $ \s -> s { terminatorState = terminatorState s { bricksEditor = newEditor } }

A couple of other comments:

refaelsh commented 1 year ago

Thank you, thank you very much! I will read it thoroughly and try to understand. I will close this non-issue in the meantime.

refaelsh commented 1 year ago

(As an aside, the then branch currently does nothing to the editor that it is given. Not sure if that's your intention.)

Yes, it is my intention.

refaelsh commented 1 year ago

ou're using GADTs for State and TerminatorState, but you don't need GADTs for those types

Yea, I figured that much. Its just that I am using Haskell LSP and by default it sugests to use GADTs and then my OCD kicks in...

recommend you update your coding style to put commas on the beginnings of lines as I did in the above declaration. You'll see that convention used in almost all Haskell code that you come across.

Yes yes. The default of the LSP is not like this. I have a menatl note to change the settings (if it is even possible).

Four space indents are much more common in Haskell code than two spaces.

  1. "much more common" - it is a strong statement.
  2. The default of the LSP is 2 spaces.
refaelsh commented 1 year ago

modify $ \s -> s { terminatorState = terminatorState s { bricksEditor = newEditor } }

Is there any particular reason for modify instead of put?

refaelsh commented 1 year ago

(This may seem a bit involved, but if it does, that's the reason why the lens-based API exists. I know you said you don't want to use that API quite yet, and that's okay; but I wanted to point this out.)

Yes yes, I am still in the early stages of Haskell Indocrination, so I will avoid lenses for the foreseeable future.

jtdaugherty commented 1 year ago

"much more common" - it is a strong statement.

I suppose it is; it's just true in my experience, having been working in Haskell for about 15 years. If nothing else, it's what I use in all of my libraries and applications and it's what I prefer for readability.

Is there any particular reason for modify instead of put?

I think it's just that I wrote my example without seeing your get; in that case I'd recommend using put $ state { ... } instead since you already did a get. Usually I think about doing modify instead if at all possible because it avoids the problem of getting mixed up with other intermediate results from get and then putting the wrong one. It also helps write code in a more functional style since you can just pass a modification function to modify.

Hope that helps!

refaelsh commented 1 year ago

I've read it like 10 times. I undestand now. I managed to make my code work with the help of your answer. Thank you very much :-)

jtdaugherty commented 1 year ago

Great, I'm glad to hear it!