Search code examples
haskellio-monadtui

Getting current time during a BrickEvent Haskell


I'm relatively new to Haskell, and I'm trying to build a terminal user interface with brick. I'd like to record a timestamp every time a user input is given. To do this I wrote the following functions:

handleInputEvent :: TestState -> BrickEvent n e -> EventM n (Next TestState)
handleInputEvent s i =
  case i of
    VtyEvent vtye ->
      case vtye of
        EvKey KBS [] -> handleBackSpaceInput s
        EvKey (KChar 'q') [MCtrl] -> halt s
        EvKey (KChar c) [] -> handleTextInput s c
        _ -> continue s
    _ -> continue s
handleTextInput :: TestState -> Char -> EventM n (Next TestState)
handleTextInput s c =
  case c of
    ' ' -> do
      let cursor = text s
      case nonEmptyCursorSelectNext cursor of
        Nothing -> continue s
        Just cursor' -> continue $ s {text = cursor'}
    _ -> do
      let tstamp = getCurrentTime
      let cursor = text s
      let cur_word = nonEmptyCursorCurrent cursor
      let new_word = TestWord {word = word cur_word, input = input cur_word ++ [c]}
      let new_text = reverse (nonEmptyCursorPrev cursor) ++ [new_word] ++ nonEmptyCursorNext cursor
      let test_event = TestEvent {timestamp = tstamp, correct = isInputCorrect cur_word c}
      case NE.nonEmpty new_text of
        Nothing -> continue s
        Just ne -> do
          case makeNonEmptyCursorWithSelection (cursorPosition cursor) ne of
            Nothing -> continue s
            Just ne' -> continue $ s {text = ne', tevents = test_event : tevents s}
data TestEvent = TestEvent
  { timestamp :: IO UTCTime,
    correct :: Bool
  }

When I now try to evaluate the difference between the first and last timestamp I get almost 0, no matter how long the program runs.

ui = do
  initialState <- buildInitialState 50
  endState <- defaultMain htyper initialState
  startTime <- timestamp (head (tevents endState))
  stopTime <- timestamp (last (tevents endState))
  let (timespan, _) = properFraction (diffUTCTime stopTime startTime)
  print timespan

I think this is because getCurrentTime returns IO UTCTime but the handler Functions themselves are not IO Functions, so the timestamps are only evaluated during the ui block. Is there any way to correctly implement this functionality in brick without rebuilding the entire code?


Solution

  • let tstamp = getCurrentTime says “define an action tstamp :: IO UTCTime which is the same as the action getCurrentTime :: IO UTCTime”. You store this value in the timestamp :: IO UTCTime field of every TestEvent, so this:

    startTime <- timestamp (head (tevents endState))
    stopTime <- timestamp (last (tevents endState))
    

    Ends up exactly equivalent to this:

    startTime <- getCurrentTime
    stopTime <- getCurrentTime
    

    And I expect it’s obvious why you see a near-0 duration between them!

    Instead, you should make the type of the timestamp field just UTCTime, and execute getCurrentTime in your event handler using liftIO to run an IO action inside Brick’s EventM:

    tstamp <- liftIO getCurrentTime
    

    Then you can extract the start and end times without I/O:

    let startTime = timestamp (head (tevents endState))
    let stopTime = timestamp (last (tevents endState))
    

    This is a common struggle for people new to Haskell: IO X isn’t a value of type X that came from doing I/O, it’s a program that may use I/O to make a value of type X.