-- This file is part of purebred
-- Copyright (C) 2017-2021 RĂ³man Joost and Fraser Tweedale
--
-- purebred is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU Affero General Public License for more details.
--
-- You should have received a copy of the GNU Affero General Public License
-- along with this program. If not, see .
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}
module Purebred.UI.Keybindings (
-- * API
dispatch
-- * Event Handlers
-- $eventhandlers
, nullEventHandler
, eventHandlerComposeFrom
, eventHandlerComposeTo
, eventHandlerComposeCc
, eventHandlerComposeBcc
, eventHandlerComposeSubject
, eventHandlerThreadComposeFrom
, eventHandlerThreadComposeTo
, eventHandlerThreadComposeSubject
, eventHandlerManageThreadTagsEditor
, eventHandlerMailAttachmentPipeToEditor
, eventHandlerMailAttachmentOpenWithEditor
, eventHandlerMailsListOfAttachments
, eventHandlerListOfThreads
, eventHandlerViewMailManageMailTagsEditor
, eventHandlerSearchThreadsEditor
, eventHandlerComposeListOfAttachments
, eventHandlerManageFileBrowserSearchPath
, eventHandlerConfirm
, eventHandlerScrollingMailView
, eventHandlerScrollingHelpView
, eventHandlerComposeFileBrowser
, eventHandlerScrollingMailViewFind
, eventHandlerSaveToDiskEditor
, eventHandlerViewMailComposeTo
) where
import qualified Brick.Types as Brick
import qualified Brick.Widgets.Edit as E
import qualified Brick.Widgets.List as L
import qualified Brick.Widgets.FileBrowser as FB
import Brick.Widgets.Dialog (handleDialogEvent)
import Graphics.Vty (Event (..))
import Control.Lens (Getter, _Left, assign, preview, to, use, view)
import Control.Monad.State
import Data.Attoparsec.Text (parseOnly)
import Data.List (find)
import Data.Text.Zipper (currentLine)
import Data.Text.Zipper.Generic (GenericTextZipper)
import qualified Data.Text as T
import Prelude hiding (readFile, unlines)
import Data.IMF.Text (mailboxList, addressList)
import Purebred.Storage.Tags (parseTagOps)
import Purebred.Types
import Purebred.Types.Parser.Text (niceEndOfInput)
import Purebred.UI.Validation (dispatchValidation)
import Purebred.UI.Widgets (editEditorL)
import Purebred.UI.Notifications (makeWarning)
-- | Purebreds event handler. Either we can look up a function
-- declared for the key press or send the key press to the Brick widget
-- to handle it.
--
data EventHandler v m = EventHandler
(forall f. Functor f
=> ([Keybinding v m] -> f [Keybinding v m])
-> AppState -> f AppState) -- lens to keybindings
(Event -> Brick.EventM Name AppState ()) -- fallback handler
lookupKeybinding :: Event -> [Keybinding v ctx] -> Maybe (Keybinding v ctx)
lookupKeybinding e = find (\x -> view kbEvent x == e)
dispatch :: EventHandler v m -> Event -> Brick.EventM Name AppState ()
dispatch (EventHandler l fallback) ev = do
kbs <- use l
case lookupKeybinding ev kbs of
Just kb -> assign asUserMessage Nothing *> view (kbAction . aAction) kb
Nothing -> fallback ev
-- | Wrapper for @Brick.Widgets.Edit.handleEditorEvent@ that takes
-- @Graphics.Vty.Event@.
--
-- /brick-0.72/ changed the type of @handleEditorEvent@ to take a
-- 'BrickEvent' rather than vty 'Event'. We implemented this wrapper
-- rather than the more invasive approach of changing the event type
-- we pass around in our event handling framework.
--
-- We can make the bigger change in the future, e.g. if we want our
-- widgts to handle mouse events.
--
handleEditorVtyEvent
:: (Eq n, E.DecodeUtf8 t, Eq t, GenericTextZipper t)
=> Graphics.Vty.Event -> Brick.EventM n (E.Editor t n) ()
handleEditorVtyEvent = E.handleEditorEvent . Brick.VtyEvent
-- | Simple wrapper around the validation function to not repeating
-- myself pulling the text values out of the lens.
--
runValidation
:: (Monoid a, MonadIO m, MonadState AppState m)
=> (a -> Maybe UserMessage) -- ^ validation
-> Getter AppState (E.Editor a n) -- ^ lens to retrieve the text used for validation
-> m ()
runValidation fx l = do
v <- use (l . E.editContentsL . to currentLine)
dispatchValidation fx v
-- $eventhandlers
-- Each event handler is handling a single widget in Purebreds UI
-- | Handlers capable of running used in more than one view
--
composeFromHandler, composeToHandler, composeCcHandler, composeBccHandler, manageMailTagHandler
:: Event -> Brick.EventM Name AppState ()
composeFromHandler e = do
Brick.zoom (asCompose . cFrom . editEditorL) (handleEditorVtyEvent e)
runValidation
(preview (_Left . to (makeWarning ComposeFrom . T.pack)) . parseOnly (mailboxList <* niceEndOfInput))
(asCompose . cFrom . editEditorL)
composeToHandler e = do
Brick.zoom (asCompose . cTo . editEditorL) (handleEditorVtyEvent e)
runValidation
(preview (_Left . to (makeWarning ComposeTo . T.pack)) . parseOnly (addressList <* niceEndOfInput))
(asCompose . cTo . editEditorL)
composeCcHandler e = do
Brick.zoom (asCompose . cCc . editEditorL) (handleEditorVtyEvent e)
runValidation
(preview (_Left . to (makeWarning ComposeCc . T.pack)) . parseOnly (addressList <* niceEndOfInput))
(asCompose . cCc . editEditorL)
composeBccHandler e = do
Brick.zoom (asCompose . cBcc . editEditorL) (handleEditorVtyEvent e)
runValidation
(preview (_Left . to (makeWarning ComposeBcc . T.pack)) . parseOnly (addressList <* niceEndOfInput))
(asCompose . cBcc . editEditorL)
manageMailTagHandler e = do
Brick.zoom (asThreadsView . miMailTagsEditor) (handleEditorVtyEvent e)
runValidation (preview _Left . parseTagOps) (asThreadsView . miMailTagsEditor)
-- | Do nothing. It might be worthwhile to enhance this to display
-- a message like "no binding for key ".
--
nullEventHandler :: EventHandler v m
nullEventHandler = EventHandler (\f s -> s <$ f []) (\_e -> pure ())
eventHandlerListOfThreads :: EventHandler 'Threads 'ListOfThreads
eventHandlerListOfThreads = EventHandler
(asConfig . confIndexView . ivBrowseThreadsKeybindings)
(Brick.zoom (asThreadsView . miListOfThreads) . L.handleListEvent)
eventHandlerSearchThreadsEditor :: EventHandler 'Threads 'SearchThreadsEditor
eventHandlerSearchThreadsEditor = EventHandler
(asConfig . confIndexView . ivSearchThreadsKeybindings)
(Brick.zoom (asThreadsView . miSearchThreadsEditor . editEditorL) . handleEditorVtyEvent)
eventHandlerViewMailManageMailTagsEditor :: EventHandler 'ViewMail 'ManageMailTagsEditor
eventHandlerViewMailManageMailTagsEditor = EventHandler
(asConfig . confMailView . mvManageMailTagsKeybindings)
manageMailTagHandler
eventHandlerMailsListOfAttachments:: EventHandler 'ViewMail 'MailListOfAttachments
eventHandlerMailsListOfAttachments = EventHandler
(asConfig . confMailView . mvMailListOfAttachmentsKeybindings)
(Brick.zoom (asMailView . mvAttachments) . L.handleListEvent)
eventHandlerMailAttachmentOpenWithEditor :: EventHandler 'ViewMail 'MailAttachmentOpenWithEditor
eventHandlerMailAttachmentOpenWithEditor = EventHandler
(asConfig . confMailView . mvOpenWithKeybindings)
(Brick.zoom (asMailView . mvOpenCommand) . handleEditorVtyEvent)
eventHandlerMailAttachmentPipeToEditor :: EventHandler 'ViewMail 'MailAttachmentPipeToEditor
eventHandlerMailAttachmentPipeToEditor = EventHandler
(asConfig . confMailView . mvPipeToKeybindings)
(Brick.zoom (asMailView . mvPipeCommand) . handleEditorVtyEvent)
eventHandlerSaveToDiskEditor :: EventHandler 'ViewMail 'SaveToDiskPathEditor
eventHandlerSaveToDiskEditor = EventHandler
(asConfig . confMailView . mvSaveToDiskKeybindings)
(Brick.zoom (asMailView . mvSaveToDiskPath) . handleEditorVtyEvent)
eventHandlerManageThreadTagsEditor :: EventHandler 'Threads 'ManageThreadTagsEditor
eventHandlerManageThreadTagsEditor =
EventHandler
(asConfig . confIndexView . ivManageThreadTagsKeybindings)
(\e -> do
Brick.zoom (asThreadsView . miThreadTagsEditor) (handleEditorVtyEvent e)
runValidation
(preview _Left . parseTagOps)
(asThreadsView . miThreadTagsEditor)
)
eventHandlerScrollingMailView :: EventHandler 'ViewMail 'ScrollingMailView
eventHandlerScrollingMailView = EventHandler
(asConfig . confMailView . mvKeybindings)
(\_e -> pure ())
eventHandlerScrollingMailViewFind :: EventHandler 'ViewMail 'ScrollingMailViewFindWordEditor
eventHandlerScrollingMailViewFind = EventHandler
(asConfig . confMailView . mvFindWordEditorKeybindings)
(Brick.zoom (asMailView . mvFindWordEditor) . handleEditorVtyEvent)
eventHandlerScrollingHelpView :: EventHandler 'Help 'ScrollingHelpView
eventHandlerScrollingHelpView = EventHandler
(asConfig . confHelpView . hvKeybindings)
(\_e -> pure ())
eventHandlerThreadComposeFrom :: EventHandler 'Threads 'ComposeFrom
eventHandlerThreadComposeFrom = EventHandler
(asConfig . confIndexView . ivFromKeybindings)
composeFromHandler
eventHandlerThreadComposeTo :: EventHandler 'Threads 'ComposeTo
eventHandlerThreadComposeTo = EventHandler
(asConfig . confIndexView . ivToKeybindings)
composeToHandler
eventHandlerThreadComposeSubject :: EventHandler 'Threads 'ComposeSubject
eventHandlerThreadComposeSubject = EventHandler
(asConfig . confIndexView . ivSubjectKeybindings)
(Brick.zoom (asCompose . cSubject . editEditorL) . handleEditorVtyEvent)
eventHandlerComposeFrom :: EventHandler 'ComposeView 'ComposeFrom
eventHandlerComposeFrom = EventHandler
(asConfig . confComposeView . cvFromKeybindings)
composeFromHandler
eventHandlerComposeTo :: EventHandler 'ComposeView 'ComposeTo
eventHandlerComposeTo = EventHandler
(asConfig . confComposeView . cvToKeybindings)
composeToHandler
eventHandlerComposeCc :: EventHandler 'ComposeView 'ComposeCc
eventHandlerComposeCc = EventHandler
(asConfig . confComposeView . cvCcKeybindings)
composeCcHandler
eventHandlerComposeBcc :: EventHandler 'ComposeView 'ComposeBcc
eventHandlerComposeBcc = EventHandler
(asConfig . confComposeView . cvBccKeybindings)
composeBccHandler
eventHandlerComposeSubject :: EventHandler 'ComposeView 'ComposeSubject
eventHandlerComposeSubject = EventHandler
(asConfig . confComposeView . cvSubjectKeybindings)
(Brick.zoom (asCompose . cSubject . editEditorL) . handleEditorVtyEvent)
eventHandlerConfirm :: EventHandler 'ComposeView 'ConfirmDialog
eventHandlerConfirm = EventHandler
(asConfig . confComposeView . cvConfirmKeybindings)
(Brick.zoom (asCompose . cKeepDraft) . handleDialogEvent)
eventHandlerComposeListOfAttachments :: EventHandler 'ComposeView 'ComposeListOfAttachments
eventHandlerComposeListOfAttachments = EventHandler
(asConfig . confComposeView . cvListOfAttachmentsKeybindings)
(Brick.zoom (asCompose . cAttachments) . L.handleListEvent)
eventHandlerComposeFileBrowser :: EventHandler 'FileBrowser 'ListOfFiles
eventHandlerComposeFileBrowser = EventHandler
(asConfig . confFileBrowserView . fbKeybindings)
(Brick.zoom (asFileBrowser . fbEntries) . FB.handleFileBrowserEvent)
eventHandlerManageFileBrowserSearchPath :: EventHandler 'FileBrowser 'ManageFileBrowserSearchPath
eventHandlerManageFileBrowserSearchPath = EventHandler
(asConfig . confFileBrowserView . fbSearchPathKeybindings)
(Brick.zoom (asFileBrowser . fbSearchPath . editEditorL) . handleEditorVtyEvent)
eventHandlerViewMailComposeTo :: EventHandler 'ViewMail 'ComposeTo
eventHandlerViewMailComposeTo = EventHandler
(asConfig . confMailView . mvToKeybindings)
composeToHandler