matterhorn-50200.0.0: src/State/ChannelSelect.hs
{-# LANGUAGE MultiWayIf #-}
module State.ChannelSelect
( beginChannelSelect
, updateChannelSelectMatches
, channelSelectNext
, channelSelectPrevious
)
where
import Prelude ()
import Prelude.MH
import Data.Char ( isUpper )
import Data.List ( findIndex )
import qualified Data.Text as T
import Lens.Micro.Platform
import Types
beginChannelSelect :: MH ()
beginChannelSelect = do
setMode ChannelSelect
csChannelSelectState .= emptyChannelSelectState
-- Select the next match in channel selection mode.
channelSelectNext :: MH ()
channelSelectNext = updateSelectedMatch succ
-- Select the previous match in channel selection mode.
channelSelectPrevious :: MH ()
channelSelectPrevious = updateSelectedMatch pred
updateChannelSelectMatches :: MH ()
updateChannelSelectMatches = do
-- Given the current channel select string, find all the channel and
-- user matches and then update the match lists.
input <- use (csChannelSelectState.channelSelectInput)
let pat = parseChannelSelectPattern input
chanNameMatches = case pat of
Nothing -> const Nothing
Just p -> if T.null input
then const Nothing
else applySelectPattern p
patTy = case pat of
Nothing -> Nothing
Just (CSP ty _) -> Just ty
chanNames <- gets (sort . allChannelNames)
uList <- use (to sortedUserList)
displayNick <- use (to useNickname)
let chanMatches = if patTy == Just UsersOnly
then mempty
else catMaybes (fmap chanNameMatches chanNames)
displayName uInf
| displayNick = uInf^.uiNickName.non (uInf^.uiName)
| otherwise = uInf^.uiName
usernameMatches = if patTy == Just ChannelsOnly
then mempty
else catMaybes (fmap (chanNameMatches . displayName) uList)
newInput <- use (csChannelSelectState.channelSelectInput)
csChannelSelectState.channelMatches .= chanMatches
csChannelSelectState.userMatches .= usernameMatches
csChannelSelectState.selectedMatch %= \oldMatch ->
-- If the user input exactly matches one of the matches, prefer
-- that one. Otherwise, if the previously selected match is
-- still a possible match, leave it selected. Otherwise revert
-- to the first available match.
let unames = matchFull <$> usernameMatches
cnames = matchFull <$> chanMatches
firstAvailableMatch =
if null chanMatches
then if null unames
then Nothing
else Just $ UserMatch $ head unames
else Just $ ChannelMatch $ head cnames
newMatch = case oldMatch of
Just (UserMatch u) ->
if newInput `elem` unames
then Just $ UserMatch newInput
else if u `elem` unames
then oldMatch
else firstAvailableMatch
Just (ChannelMatch c) ->
if newInput `elem` cnames
then Just $ ChannelMatch $ newInput
else if c `elem` cnames
then oldMatch
else firstAvailableMatch
Nothing -> firstAvailableMatch
in newMatch
applySelectPattern :: ChannelSelectPattern -> Text -> Maybe ChannelSelectMatch
applySelectPattern (CSP ty pat) chanName = do
let applyType Infix | pat `T.isInfixOf` normalizedChanName =
case T.breakOn pat normalizedChanName of
(pre, _) ->
return ( T.take (T.length pre) chanName
, T.take (T.length pat) $ T.drop (T.length pre) chanName
, T.drop (T.length pat + T.length pre) chanName
)
applyType Prefix | pat `T.isPrefixOf` normalizedChanName = do
let (b, a) = T.splitAt (T.length pat) chanName
return ("", b, a)
applyType UsersOnly | pat `T.isPrefixOf` normalizedChanName = do
let (b, a) = T.splitAt (T.length pat) chanName
return ("", b, a)
applyType ChannelsOnly | pat `T.isPrefixOf` normalizedChanName = do
let (b, a) = T.splitAt (T.length pat) chanName
return ("", b, a)
applyType Suffix | pat `T.isSuffixOf` normalizedChanName = do
let (b, a) = T.splitAt (T.length chanName - T.length pat) chanName
return (b, a, "")
applyType Equal | pat == normalizedChanName =
return ("", chanName, "")
applyType _ = Nothing
caseSensitive = T.any isUpper pat
normalizedChanName = if caseSensitive
then chanName
else T.toLower chanName
(pre, m, post) <- applyType ty
return $ ChannelSelectMatch pre m post chanName
parseChannelSelectPattern :: Text -> Maybe ChannelSelectPattern
parseChannelSelectPattern pat = do
let only = if | userSigil `T.isPrefixOf` pat -> Just $ CSP UsersOnly $ T.tail pat
| normalChannelSigil `T.isPrefixOf` pat -> Just $ CSP ChannelsOnly $ T.tail pat
| otherwise -> Nothing
(pat1, pfx) <- case "^" `T.isPrefixOf` pat of
True -> return (T.tail pat, Just Prefix)
False -> return (pat, Nothing)
(pat2, sfx) <- case "$" `T.isSuffixOf` pat1 of
True -> return (T.init pat1, Just Suffix)
False -> return (pat1, Nothing)
only <|> case (pfx, sfx) of
(Nothing, Nothing) -> return $ CSP Infix pat2
(Just Prefix, Nothing) -> return $ CSP Prefix pat2
(Nothing, Just Suffix) -> return $ CSP Suffix pat2
(Just Prefix, Just Suffix) -> return $ CSP Equal pat2
tys -> error $ "BUG: invalid channel select case: " <> show tys
-- Update the channel selection mode match cursor. The argument function
-- determines how the new cursor position is computed from the old
-- one. The new cursor position is automatically wrapped around to the
-- beginning or end of the channel selection match list, so cursor
-- transformations do not have to do index validation. If the current
-- match (e.g. the sentinel "") is not found in the match list, this
-- sets the cursor position to the first match, if any.
updateSelectedMatch :: (Int -> Int) -> MH ()
updateSelectedMatch nextIndex = do
chanMatches <- use (csChannelSelectState.channelMatches)
usernameMatches <- use (csChannelSelectState.userMatches)
csChannelSelectState.selectedMatch %= \oldMatch ->
-- Make the list of all matches, in display order.
let allMatches = concat [ (ChannelMatch . matchFull) <$> chanMatches
, (UserMatch . matchFull) <$> usernameMatches
]
defaultMatch = if null allMatches
then Nothing
else Just $ allMatches !! 0
in case oldMatch of
Nothing -> defaultMatch
Just oldMatch' -> case findIndex (== oldMatch') allMatches of
Nothing -> defaultMatch
Just i ->
let newIndex = if tmpIndex < 0
then length allMatches - 1
else if tmpIndex >= length allMatches
then 0
else tmpIndex
tmpIndex = nextIndex i
in Just $ allMatches !! newIndex