ribosome-0.4.0.0: lib/Ribosome/Menu/Simple.hs
module Ribosome.Menu.Simple where
import Control.Lens (_2, element, ifolded, over, set, toListOf, view, withIndex, (^..), (^?))
import qualified Control.Lens as Lens (filtered)
import Data.Composition ((.:))
import Data.Map.Strict ((!?))
import qualified Data.Map.Strict as Map (fromList, union)
import qualified Data.Ord as Ord (Down(Down))
import qualified Data.Text as Text (breakOn, null)
import qualified Text.Fuzzy as Fuzzy (Fuzzy(score, original), filter)
import Ribosome.Data.List (indexesComplement)
import Ribosome.Menu.Action (menuContinue, menuCycle, menuQuitWith, menuToggle, menuToggleAll)
import Ribosome.Menu.Data.BasicMenuAction (BasicMenuAction, BasicMenuChange)
import qualified Ribosome.Menu.Data.BasicMenuAction as BasicMenuAction (BasicMenuAction(..))
import qualified Ribosome.Menu.Data.BasicMenuAction as BasicMenuChange (BasicMenuChange(..))
import Ribosome.Menu.Data.FilteredMenuItem (FilteredMenuItem(FilteredMenuItem))
import qualified Ribosome.Menu.Data.FilteredMenuItem as FilteredMenuItem (index, item)
import Ribosome.Menu.Data.Menu (Menu(Menu), MenuFilter(MenuFilter))
import qualified Ribosome.Menu.Data.Menu as Menu (currentFilter, filtered, items, marked, 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 (text)
import Ribosome.Menu.Data.MenuItemFilter (MenuItemFilter(MenuItemFilter))
import Ribosome.Menu.Data.MenuUpdate (MenuUpdate(MenuUpdate))
import Ribosome.Menu.Prompt.Data.Prompt (Prompt(Prompt))
type MappingHandler m a i = Menu i -> Prompt -> m (MenuConsumerAction m a, Menu i)
type Mappings m a i = Map Text (MappingHandler m a i)
zipWithIndex :: [a] -> [(Int, a)]
zipWithIndex =
toListOf $ ifolded . withIndex
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
substringMenuItemMatcher :: MenuItemFilter a
substringMenuItemMatcher =
MenuItemFilter filt
where
filt text =
uncurry FilteredMenuItem <$$> matcher text
matcher text =
toListOf $ ifolded . Lens.filtered (textContains text . view MenuItem.text) . withIndex
fuzzyMenuItemMatcher :: MenuItemFilter a
fuzzyMenuItemMatcher =
MenuItemFilter matcher
where
matcher =
fmap (uncurry FilteredMenuItem . Fuzzy.original) . sortOn (Ord.Down . Fuzzy.score) .: filtered
filtered text items =
Fuzzy.filter text (items ^.. ifolded . withIndex) "" "" (view (_2 . MenuItem.text)) False
menuItemsNonequal :: [FilteredMenuItem i] -> [FilteredMenuItem i] -> Bool
menuItemsNonequal a b =
(view FilteredMenuItem.index <$> a) /= (view FilteredMenuItem.index <$> b)
updateFilter :: MenuItemFilter i -> Text -> Menu i -> (BasicMenuChange, Menu i)
updateFilter (MenuItemFilter itemFilter) text menu@(Menu items oldFiltered _ _ _ _) =
(change, update menu)
where
change =
if menuItemsNonequal filtered oldFiltered
then BasicMenuChange.Change
else BasicMenuChange.NoChange
update =
set Menu.filtered filtered . set Menu.currentFilter (MenuFilter text)
filtered =
itemFilter text items
reapplyFilter :: MenuItemFilter i -> Menu i -> (BasicMenuChange, Menu i)
reapplyFilter itemFilter menu@(Menu _ _ _ _ (MenuFilter currentFilter) _) =
updateFilter itemFilter currentFilter menu
basicMenuTransform :: MenuItemFilter i -> MenuEvent m a i -> Menu i -> BasicMenuAction m a i
basicMenuTransform matcher (MenuEvent.PromptChange (Prompt _ _ text)) =
BasicMenuAction.Continue BasicMenuChange.Reset . snd . updateFilter matcher text
basicMenuTransform _ (MenuEvent.Mapping _ _) =
BasicMenuAction.Continue BasicMenuChange.NoChange
basicMenuTransform matcher (MenuEvent.NewItems items) =
uncurry BasicMenuAction.Continue . reapplyFilter matcher . over Menu.items (++ items)
basicMenuTransform _ (MenuEvent.Init _) =
BasicMenuAction.Continue BasicMenuChange.Change
basicMenuTransform _ (MenuEvent.Quit reason) =
const $ BasicMenuAction.Quit reason
resetSelection :: BasicMenuChange -> Menu i -> Menu i
resetSelection BasicMenuChange.Reset (Menu i f _ _ filt mi) =
Menu i f 0 [] filt mi
resetSelection _ m =
m
menuAction ::
MenuItemFilter i ->
Bool ->
MenuConsumerAction m a ->
Menu i ->
(MenuAction m a, Menu i)
menuAction itemFilter _ MenuConsumerAction.Filter menu =
(MenuAction.Render True, uncurry resetSelection $ reapplyFilter itemFilter menu)
menuAction _ True MenuConsumerAction.Continue menu =
(MenuAction.Render True, menu)
menuAction _ False MenuConsumerAction.Continue menu =
(MenuAction.Continue, menu)
menuAction _ _ (MenuConsumerAction.Execute thunk) menu =
(MenuAction.Execute thunk, menu)
menuAction _ basicChanged (MenuConsumerAction.Render consumerChanged) menu =
(MenuAction.Render (basicChanged || consumerChanged), menu)
menuAction _ _ (MenuConsumerAction.QuitWith ma) menu =
(MenuAction.Quit (QuitReason.Execute ma), menu)
menuAction _ _ MenuConsumerAction.Quit menu =
(MenuAction.Quit QuitReason.Aborted, menu)
menuAction _ _ (MenuConsumerAction.Return a) menu =
(MenuAction.Quit (QuitReason.Return a), menu)
menuAction _ _ (MenuConsumerAction.UpdatePrompt prompt) menu =
(MenuAction.UpdatePrompt prompt, menu)
basicMenuAction ::
Monad m =>
MenuItemFilter i ->
(MenuUpdate m a i -> m (MenuConsumerAction m a, Menu i)) ->
MenuUpdate m a i ->
BasicMenuAction m a i ->
m (MenuAction m a, Menu i)
basicMenuAction itemFilter consumer (MenuUpdate event menu) =
act
where
act (BasicMenuAction.Quit reason) =
pure (MenuAction.Quit reason, menu)
act (BasicMenuAction.Continue change m) = do
(action, updatedMenu) <- consumerTransform (resetSelection change m)
return $ menuAction itemFilter (change /= BasicMenuChange.NoChange) action updatedMenu
consumerTransform newMenu =
consumer (MenuUpdate event newMenu)
basicMenu ::
Monad m =>
MenuItemFilter i ->
(MenuUpdate m a i -> m (MenuConsumerAction m a, Menu i)) ->
MenuUpdate m a i ->
m (MenuAction m a, Menu i)
basicMenu itemFilter consumer update@(MenuUpdate event menu) =
basicMenuAction itemFilter consumer update basicTransform
where
basicTransform =
basicMenuTransform itemFilter event menu
mappingConsumer ::
Monad m =>
Mappings m a i ->
MenuUpdate m a i ->
m (MenuConsumerAction m a, Menu i)
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 i ->
MenuUpdate m a i ->
m (MenuAction m a, Menu i)
simpleMenu =
basicMenu fuzzyMenuItemMatcher . mappingConsumer
defaultMappings ::
Monad m =>
Mappings m a i
defaultMappings =
Map.fromList [
("k", menuCycle 1),
("c-k", menuCycle 1),
("j", menuCycle (-1)),
("c-j", menuCycle (-1)),
("space", menuToggle),
("*", menuToggleAll)
]
defaultMenu ::
Monad m =>
Mappings m a i ->
MenuUpdate m a i ->
m (MenuAction m a, Menu i)
defaultMenu =
simpleMenu . (`Map.union` defaultMappings)
selectedMenuItem :: Menu i -> Maybe (MenuItem i)
selectedMenuItem (Menu _ filtered selected _ _ _) =
filtered ^? element selected . FilteredMenuItem.item
withSelectedMenuItem ::
Monad m =>
(MenuItem i -> m (MenuConsumerAction m a, Menu i)) ->
Menu i ->
m (MenuConsumerAction m a, Menu i)
withSelectedMenuItem f m =
maybe (menuContinue m) pure =<< traverse f (selectedMenuItem m)
filterIndexes :: [Int] -> [a] -> [a]
filterIndexes indexes =
reverse . go 0 (sort indexes) []
where
go current (i : is) result (a : asTail) | i == current =
go (current + 1) is (a : result) asTail
go current is result (_ : asTail) =
go (current + 1) is result asTail
go _ _ result _ =
result
markedIndexes :: Menu i -> [Int]
markedIndexes (Menu _ _ selected [] _ _) =
[selected]
markedIndexes (Menu _ _ _ marked _ _) =
marked
menuItemsByIndexes :: [Int] -> Menu i -> [MenuItem i]
menuItemsByIndexes indexes (Menu _ filtered _ _ _ _) =
view FilteredMenuItem.item <$> filterIndexes indexes filtered
markedMenuItemsOnly :: Menu i -> Maybe (NonEmpty (MenuItem i))
markedMenuItemsOnly (Menu _ _ _ [] _ _) =
Nothing
markedMenuItemsOnly (Menu _ filtered _ marked _ _) =
nonEmpty $ view FilteredMenuItem.item <$> filterIndexes marked filtered
markedMenuItems :: Menu i -> Maybe (NonEmpty (MenuItem i))
markedMenuItems m =
markedMenuItemsOnly m <|> (pure <$> selectedMenuItem m)
unmarkedMenuItems :: Menu i -> [MenuItem i]
unmarkedMenuItems menu =
menuItemsByIndexes (indexesComplement (length (view Menu.filtered menu)) (indexes menu)) menu
where
indexes (Menu _ _ selected [] _ _) =
[selected]
indexes (Menu _ _ _ marked _ _) =
marked
withMarkedMenuItems ::
Monad m =>
(NonEmpty (MenuItem i) -> m a) ->
Menu i ->
m (Maybe a)
withMarkedMenuItems f m =
traverse f (markedMenuItems m)
withMarkedMenuItems_ ::
Monad m =>
(NonEmpty (MenuItem i) -> m ()) ->
Menu i ->
m ()
withMarkedMenuItems_ f m =
traverse_ f (markedMenuItems m)
actionWithMarkedMenuItems ::
Monad m =>
(m (NonEmpty b) -> Menu i -> m (MenuConsumerAction m a, Menu i)) ->
(MenuItem i -> m b) ->
Menu i ->
m (MenuConsumerAction m a, Menu i)
actionWithMarkedMenuItems next f m =
fromMaybe (MenuConsumerAction.Continue, m) <$> withMarkedMenuItems run m
where
run items =
next (traverse f items) m
traverseMarkedMenuItems ::
Monad m =>
(MenuItem i -> m a) ->
Menu i ->
m (Maybe (NonEmpty a))
traverseMarkedMenuItems =
withMarkedMenuItems . traverse
traverseMarkedMenuItems_ ::
Monad m =>
(MenuItem i -> m ()) ->
Menu i ->
m ()
traverseMarkedMenuItems_ =
withMarkedMenuItems_ . traverse_
traverseMarkedMenuItemsAndQuit ::
Monad m =>
(MenuItem i -> m a) ->
Menu i ->
m (MenuConsumerAction m (NonEmpty a), Menu i)
traverseMarkedMenuItemsAndQuit =
actionWithMarkedMenuItems menuQuitWith
traverseMarkedMenuItemsAndQuit_ ::
Monad m =>
(MenuItem i -> m ()) ->
Menu i ->
m (MenuConsumerAction m (), Menu i)
traverseMarkedMenuItemsAndQuit_ f =
first void <$$> actionWithMarkedMenuItems menuQuitWith f
deleteByFilteredIndex :: [Int] -> Menu i -> Menu i
deleteByFilteredIndex indexes menu@(Menu items filtered _ _ _ _) =
set Menu.items newItems . set Menu.filtered [] $ menu
where
newItems =
filterIndexes (indexesComplement (length items) unfilteredIndexes) items
unfilteredIndexes =
view FilteredMenuItem.index <$> filterIndexes indexes filtered
deleteMarked :: Menu i -> Menu i
deleteMarked menu =
set Menu.selected 0 . set Menu.marked [] . deleteByFilteredIndex (markedIndexes menu) $ menu