packages feed

ribosome-0.3.0.0: lib/Ribosome/Menu/Simple.hs

module Ribosome.Menu.Simple where

import Control.Lens ((^?))
import qualified Control.Lens as Lens (element, over, views)
import Data.Composition ((.:))
import Data.Map ((!?))
import qualified Data.Map as Map (fromList, union)
import qualified Data.Text as Text (breakOn, null)

import Ribosome.Menu.Data.Menu (Menu(Menu), MenuFilter(MenuFilter))
import qualified Ribosome.Menu.Data.Menu as Menu (filtered, items, selected)
import Ribosome.Menu.Data.MenuAction (MenuAction)
import qualified Ribosome.Menu.Data.MenuAction as MenuAction (MenuAction(..))
import Ribosome.Menu.Data.MenuConsumerAction (MenuConsumerAction)
import qualified Ribosome.Menu.Data.MenuConsumerAction as MenuConsumerAction (MenuConsumerAction(..))
import Ribosome.Menu.Data.MenuEvent (MenuEvent)
import qualified Ribosome.Menu.Data.MenuEvent as MenuEvent (MenuEvent(..))
import qualified Ribosome.Menu.Data.MenuEvent as QuitReason (QuitReason(..))
import Ribosome.Menu.Data.MenuItem (MenuItem)
import qualified Ribosome.Menu.Data.MenuItem as MenuItem (MenuItem(_text))
import Ribosome.Menu.Data.MenuUpdate (MenuUpdate(MenuUpdate))
import Ribosome.Menu.Prompt.Data.Prompt (Prompt(Prompt))

type Mappings m a = Map Text (Menu -> Prompt -> m (MenuConsumerAction m a, Menu))

textContains :: Text -> Text -> Bool
textContains needle haystack =
  Text.null needle || (not (Text.null haystack) && search needle haystack)
  where
    search =
      not . Text.null . snd .: Text.breakOn

updateFilter :: Text -> Menu -> (Bool, MenuAction m a, Menu)
updateFilter text (Menu items oldFiltered stack selected _) =
  (filtered /= oldFiltered, MenuAction.Continue, Menu items filtered stack selected (MenuFilter text))
  where
    filtered =
      filter (textContains text . MenuItem._text) items

reapplyFilter :: Menu -> (Bool, MenuAction m a, Menu)
reapplyFilter menu@(Menu _ _ _ _ (MenuFilter currentFilter)) =
  updateFilter currentFilter menu

basicMenuTransform :: MenuEvent m a -> Menu -> (Bool, MenuAction m a, Menu)
basicMenuTransform (MenuEvent.PromptChange _ (Prompt _ _ text)) =
  updateFilter text
basicMenuTransform (MenuEvent.Mapping _ _) =
  (False, MenuAction.Continue,)
basicMenuTransform (MenuEvent.NewItems item) =
  reapplyFilter . Lens.over Menu.items (item :)
basicMenuTransform (MenuEvent.Init _) =
  (True, MenuAction.Continue,)
basicMenuTransform (MenuEvent.Quit reason) =
  (False, MenuAction.Quit reason,)

basicMenu ::
  Monad m =>
  (MenuUpdate m a -> m (MenuConsumerAction m a, Menu)) ->
  MenuUpdate m a ->
  m (MenuAction m a, Menu)
basicMenu consumer (MenuUpdate event menu) =
  consumerAction action
  where
    (changed, action, newMenu) = basicMenuTransform event menu
    consumerAction (MenuAction.Quit reason) =
      return (MenuAction.Quit reason, menu)
    consumerAction _ =
      first menuAction <$> consumer (MenuUpdate event newMenu)
    menuAction MenuConsumerAction.Continue =
      if changed then MenuAction.Render True else MenuAction.Continue
    menuAction (MenuConsumerAction.Render consumerChanged) =
      MenuAction.Render (changed || consumerChanged)
    menuAction (MenuConsumerAction.QuitWith ma) =
      MenuAction.Quit (QuitReason.Execute ma)
    menuAction MenuConsumerAction.Quit =
      MenuAction.Quit QuitReason.Aborted
    menuAction (MenuConsumerAction.Return a) =
      MenuAction.Quit (QuitReason.Return a)

mappingConsumer ::
  Monad m =>
  Mappings m a ->
  MenuUpdate m a ->
  m (MenuConsumerAction m a, Menu)
mappingConsumer mappings (MenuUpdate (MenuEvent.Mapping char prompt) menu) =
  handler menu prompt
  where
    handler =
      fromMaybe (const . menuContinue) (mappings !? char)
mappingConsumer _ (MenuUpdate _ menu) =
  menuContinue menu

simpleMenu ::
  Monad m =>
  Mappings m a ->
  MenuUpdate m a ->
  m (MenuAction m a, Menu)
simpleMenu =
  basicMenu . mappingConsumer

menuCycle ::
  Monad m =>
  Int ->
  Menu ->
  Prompt ->
  m (MenuConsumerAction m a, Menu)
menuCycle offset m _ =
  menuRender False (Lens.over Menu.selected add m)
  where
    count =
      Lens.views Menu.filtered length m
    add current =
      if count == 0 then 0 else (current + offset) `mod` count

defaultMappings ::
  Monad m =>
  Mappings m a
defaultMappings =
  Map.fromList [("k", menuCycle 1), ("j", menuCycle (-1))]

defaultMenu ::
  Monad m =>
  Mappings m a ->
  MenuUpdate m a ->
  m (MenuAction m a, Menu)
defaultMenu =
  simpleMenu . (`Map.union` defaultMappings)

menuContinue ::
  Monad m =>
  Menu ->
  m (MenuConsumerAction m a, Menu)
menuContinue =
  return . (MenuConsumerAction.Continue,)

menuRender ::
  Monad m =>
  Bool ->
  Menu ->
  m (MenuConsumerAction m a, Menu)
menuRender changed =
  return . (MenuConsumerAction.Render changed,)

menuQuit ::
  Monad m =>
  Menu ->
  m (MenuConsumerAction m a, Menu)
menuQuit =
  return . (MenuConsumerAction.Quit,)

menuQuitWith ::
  Monad m =>
  m a ->
  Menu ->
  m (MenuConsumerAction m a, Menu)
menuQuitWith next =
  return . (MenuConsumerAction.QuitWith next,)

menuReturn ::
  Monad m =>
  a ->
  Menu ->
  m (MenuConsumerAction m a, Menu)
menuReturn a =
  return . (MenuConsumerAction.Return a,)

selectedMenuItem :: Menu -> Maybe MenuItem
selectedMenuItem (Menu _ items _ selected _) =
  items ^? Lens.element (length items - selected - 1)