-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMain.hs
More file actions
355 lines (281 loc) · 11.9 KB
/
Main.hs
File metadata and controls
355 lines (281 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
{-# Language OverloadedStrings, DataKinds, TypeOperators, LambdaCase,
ScopedTypeVariables #-}
import qualified Brick as B
import qualified Brick.Widgets.Border as B
import qualified Brick.Widgets.List as B
import qualified Brick.Widgets.Center as B
import Control.Arrow ((&&&))
import Control.Concurrent (Chan, newChan, writeChan, forkIO, ThreadId)
import Control.Exception (handle, throwTo, mask, Exception, SomeException)
import Control.Monad (void)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Either (EitherT, runEitherT)
import Data.Aeson ( FromJSON, parseJSON, ToJSON, toJSON, (.:), (.:?), (.=)
, withObject, object
)
import Data.Fixed
import qualified Data.ListTrie.Map as Trie
import qualified Data.Map as Map
import Data.Proxy
import qualified Data.Vector as V
import qualified Data.Text as Text
import Data.Typeable (Typeable)
import Formatting
import qualified Graphics.Vty as Vty
import Servant.API
import Servant.Client
-------------------------------------------------------------------------------
-- Types
-------------------------------------------------------------------------------
type ServantT = EitherT ServantError IO
-- Strichliste API ------------------------------------------------------------
data Page a = Page
{ pageOverallCount :: Int
, pageLimit :: Maybe Int
, pageOffset :: Maybe Int
, pageEntries :: V.Vector a
}
instance FromJSON a => FromJSON (Page a) where
parseJSON = withObject "page" $ \o ->
Page <$> o .: "overallCount" <*> o .:? "limit" <*> o .:? "offset" <*> o .: "entries"
type Date = Text.Text
type ID = Int
data User = User
{ userName :: Text.Text
, userID :: ID
, userBalance :: Centi
, userLastTransaction :: Maybe Date
}
instance FromJSON User where
parseJSON = withObject "user" $ \o ->
User <$> o .: "name" <*> o .: "id" <*> o .: "balance" <*> o .:? "lastTransaction"
data Transaction = Transaction
{ taValue :: Centi
, taCreateDate :: Date
, taID :: ID
, taUserID :: ID
}
instance FromJSON Transaction where
parseJSON = withObject "transaction" $ \o ->
Transaction <$> o .: "value" <*> o .: "createDate" <*> o .: "id" <*> o .: "userId"
newtype Value = Value { getValue :: Centi }
instance ToJSON Value where
toJSON (Value amount) = object [ "value" .= amount ]
type UserID = Capture "userId" ID
type Strichliste = "user" :> Get '[JSON] (Page User)
:<|> "user" :> UserID :> Get '[JSON] User
:<|> "user" :> UserID :> "transaction"
:> Get '[JSON] (Page Transaction)
:<|> "user" :> UserID :> "transaction"
:> ReqBody '[JSON] Value
:> Post '[JSON] Transaction
strichliste :: Proxy Strichliste
strichliste = Proxy
getUsers :: ServantT (Page User)
getUser :: ID -> ServantT User
getUserTransactions :: ID -> ServantT (Page Transaction)
postTransaction :: ID -> Value -> ServantT Transaction
getUsers :<|> getUser :<|> getUserTransactions :<|> postTransaction =
client strichliste host
where
host = BaseUrl Https "demo-api.strichliste.org" 443
-- Application ----------------------------------------------------------------
type Trie = Trie.TrieMap Map.Map Char
data FilterList a = FL
{ filterPrefix :: [Char]
, filterAll :: Trie a
, filterName :: B.Name
, filterCurrent :: B.List a
}
filterList :: [Char] -> Trie a -> B.Name -> FilterList a
filterList flPrefix flAll flName =
FL flPrefix flAll flName $ B.list flName (V.fromList matching) 1
where
matching = trieElems $ Trie.lookupPrefix flPrefix flAll
instance B.HandleEvent (FilterList a) where
handleEvent ev (FL flPrefix flAll flName flCurrent) = case ev of
Vty.EvKey (Vty.KChar c) [] ->
filterUsers $ flPrefix ++ [c]
Vty.EvKey Vty.KBS [] ->
filterUsers $ if null flPrefix then flPrefix else init flPrefix
_ ->
FL flPrefix flAll flName <$> B.handleEvent ev flCurrent
where
filterUsers p = return $ filterList p flAll flName
data MyEvent
= VtyEvent Vty.Event
| PurchaseSuccessful UIState
| PurchaseFailed String
data UIState
= UserMenu (FilterList User)
| TransactionMenu User -- ^ selected user
Centi -- ^ user's balance
(FilterList Centi) -- ^ possible transactions
(B.List Transaction) -- ^ past transactions
| Processing ThreadId -- ^ Thread doing the work
UIState -- ^ Previous state
| Error String -- ^ Error description
UIState -- ^ Previous state
-------------------------------------------------------------------------------
-- Drawing
-------------------------------------------------------------------------------
drawUI :: UIState -> [B.Widget]
drawUI uiState =
case uiState of
UserMenu (FL prefix _ _ users) ->
let selectW = boxedListW "Select user" users drawUserListElement
in [ B.vBox $ selectW : filterW prefix ]
TransactionMenu _ balance (FL prefix _ _ amounts) transactions ->
let selectW = boxedListW "Select amounts" amounts drawAmountListElement
balanceW = B.borderWithLabel (B.str "Balance") $ B.hCenter
$ B.txt (sformat shown balance)
transactionsW = boxedListW "Past transactions" transactions drawTransactioListElement
in [ B.hBox
[ B.vBox
$ selectW
: filterW prefix
, B.vBox
[ balanceW
, transactionsW
]
]
]
Processing _ prev ->
let w = B.hCenter $ B.border $ B.str "Processing..."
in w : drawUI prev
Error err prev ->
let errorW = B.hCenter
$ B.withAttr errorAttr
$ B.borderWithLabel (B.str "An error occured")
$ B.str err
in errorW : drawUI prev
boxedListW :: [Char] -> B.List a -> (Bool -> a -> B.Widget) -> B.Widget
boxedListW name l drawL = B.borderWithLabel (B.str name) $ B.renderList l drawL
filterW :: [Char] -> [B.Widget]
filterW prefix
| null prefix = []
| otherwise = [ B.borderWithLabel (B.str "Filter") $ B.hCenter $ B.str prefix
]
errorAttr :: B.AttrName
errorAttr = B.attrName "errorMsg"
theMap :: B.AttrMap
theMap = B.attrMap Vty.defAttr
[ (B.listAttr, Vty.white `B.on` Vty.black)
, (B.listSelectedAttr, Vty.black `B.on` Vty.white)
, (errorAttr, B.fg Vty.red)
]
drawAmountListElement :: Bool -> Centi -> B.Widget
drawAmountListElement _ amount = B.hCenter $ B.txt $ sformat shown amount
drawTransactioListElement :: Bool -> Transaction -> B.Widget
drawTransactioListElement _ (Transaction v cd _ _) =
B.hCenter $ B.txt $ sformat (stext % ": " % shown % "€") cd v
drawUserListElement :: Bool -> User -> B.Widget
drawUserListElement _ u = B.hCenter $ B.txt $ userName u
-------------------------------------------------------------------------------
-- State
-------------------------------------------------------------------------------
appEvent :: Chan MyEvent -> UIState -> MyEvent -> B.EventM (B.Next UIState)
appEvent chan uiState e =
case uiState of
UserMenu fl -> case unsafeToVtyEvent e of
Vty.EvKey Vty.KEsc _ -> B.halt uiState
Vty.EvKey Vty.KEnter _ -> chooseSelectedUser uiState fl
ev -> B.handleEvent ev fl >>= B.continue . UserMenu
TransactionMenu u balance fl transactions -> case unsafeToVtyEvent e of
Vty.EvKey Vty.KEsc _ -> toEventM uiState getUserMenu >>= B.continue
Vty.EvKey Vty.KEnter _ -> chooseSelectedAmount uiState chan u fl
ev -> do
newList <- B.handleEvent ev fl
B.continue $ TransactionMenu u balance newList transactions
Processing tid prev -> case e of
VtyEvent (Vty.EvKey Vty.KEsc _) -> abortProcessing uiState tid
VtyEvent _ -> B.continue uiState
PurchaseSuccessful newState -> B.continue newState
PurchaseFailed err -> B.continue (Error err prev)
Error _ prev -> B.continue prev -- TODO: try to redraw
unsafeToVtyEvent :: MyEvent -> Vty.Event
unsafeToVtyEvent (VtyEvent e) = e
unsafeToVtyEvent _ = error "Invalid event received!"
-- User Menu -----------------------------------------------------------------
getUserMenu :: ServantT UIState
getUserMenu = mkUserMenu <$> getUsers
mkUserMenu :: Page User -> UIState
mkUserMenu page = UserMenu $ filterList "" usersT "Users"
where
usersT = indexUsers page
indexUsers :: Page User -> Trie User
indexUsers = usersToTrie . pageEntries
usersToTrie :: V.Vector User -> Trie User
usersToTrie = Trie.fromList . V.toList . V.map (Text.unpack . userName &&& id)
chooseSelectedUser :: UIState -> FilterList User -> B.EventM (B.Next UIState)
chooseSelectedUser uiState fl = withSelectedElement uiState fl $ \u ->
toEventM uiState (getTransactionMenu u) >>= B.continue
-- Transaction Menu -----------------------------------------------------------
getTransactionMenu :: User -> ServantT UIState
getTransactionMenu u = do
u' <- getUser $ userID u
TransactionMenu u' (userBalance u') actionList <$> getTransactions u'
where
amounts = [2, 1.5, 1, 0.5, -0.5, -1, -1.5, -2.0]
amountsT = indexAmounts amounts
actionList = FL "" amountsT "Actions" (mkActionList amounts)
indexAmounts :: [Centi] -> Trie Centi
indexAmounts = Trie.fromList . map (show &&& id)
mkActionList :: [Centi] -> B.List Centi
mkActionList cs = B.list "Actions" (V.fromList cs) 1
getTransactions :: User -> ServantT (B.List Transaction)
getTransactions (User _ uid _ _) = do
pastTrans <- getUserTransactions uid
return $ B.list "Transactions" (pageEntries pastTrans) 1
chooseSelectedAmount :: UIState -> Chan MyEvent -> User -> FilterList Centi
-> B.EventM (B.Next UIState)
chooseSelectedAmount uiState chan u fl = withSelectedElement uiState fl $ \a -> do
tid <- liftIO $ purchase u a chan
B.continue $ Processing tid uiState
-- Processing -----------------------------------------------------------------
data Abort = Abort deriving (Show, Typeable)
instance Exception Abort
purchase :: MonadIO io => User -> Centi -> Chan MyEvent -> io ThreadId
purchase u@(User _ uid _ _) amount chan = liftIO $ mask $ \restore -> forkIO $
handle (\(_ :: Abort) -> writeChan chan (PurchaseFailed "Aborted")) $
handle (\(e :: SomeException) -> writeChan chan (PurchaseFailed (show e))) $
restore $
runEitherT (postTransaction uid (Value amount) >> getTransactionMenu u)
>>= writeChan chan . eitherToEvent
where
eitherToEvent (Left err) = PurchaseFailed (show err)
eitherToEvent (Right uiState) = PurchaseSuccessful uiState
abortProcessing :: UIState -> ThreadId -> B.EventM (B.Next UIState)
abortProcessing uiState tid = liftIO (throwTo tid Abort) >> B.continue uiState
-- Misc -----------------------------------------------------------------------
toEventM :: MonadIO io => UIState -> ServantT UIState -> io UIState
toEventM prev servantState = liftIO $ runEitherT servantState >>= \case
Right st -> return st
Left err -> return $ Error (show err) prev
trieElems :: Trie a -> [a]
trieElems = map snd . Trie.toAscList
emptyState :: UIState
emptyState = UserMenu $ filterList "" Trie.empty "Users"
withSelectedElement :: UIState -> FilterList a -> (a -> B.EventM (B.Next UIState))
-> B.EventM (B.Next UIState)
withSelectedElement uiState (FL _ _ _ as) f =
case B.listSelectedElement as of
Nothing -> B.continue uiState
Just (_, a) -> f a
-------------------------------------------------------------------------------
-- Main
-------------------------------------------------------------------------------
app :: Chan MyEvent -> B.App UIState MyEvent
app chan = B.App
{ B.appDraw = drawUI
, B.appStartEvent = return
, B.appHandleEvent = appEvent chan
, B.appAttrMap = const theMap
, B.appLiftVtyEvent = VtyEvent
, B.appChooseCursor = B.showFirstCursor
}
main :: IO ()
main = do
chan <- newChan
users <- toEventM emptyState $ getUserMenu
void $ B.customMain (Vty.mkVty mempty) chan (app chan) users