-
Notifications
You must be signed in to change notification settings - Fork 0
/
Main.hs
436 lines (343 loc) · 16.4 KB
/
Main.hs
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
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
module Main where
import Control.Monad
import Control.Monad.Fix
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import qualified Data.Aeson as Aeson
import Data.Bifunctor
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import Data.Semialign (align)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Zipper as Zipper
import Data.Time.Clock (UTCTime, getCurrentTime)
import Data.Time.Format (parseTimeM, defaultTimeLocale, formatTime)
import GHC.Generics
import qualified Graphics.Vty as V
import Reflex hiding (apply, Event)
import qualified Reflex
import Reflex.Network
import Reflex.Vty hiding (apply, Event, mainWidget)
import System.Environment (getArgs)
import qualified Kyowon.Client as Client
import Kyowon.Core.Types (UTCTimestamp(..), ClientId, createClient, UniqueId(..))
import Kyowon.Reflex.Client (KyowonT, runKyowonT, zeroNextId, KyowonMonad)
import qualified Kyowon.Reflex.Client as Reflex
import Kyowon.Reflex.Common (catMaybes)
import Kyowon.Reflex.Next (nextIdWith)
import Kyowon.Reflex.Time (sampleMonotonicTimeWith)
import qualified Kyowon.Reflex.VRDT.LWW as Reflex
import Kyowon.Reflex.Vty.Widget
import VRDT.Class
import VRDT.LWW (LWW(..))
import qualified VRDT.LWW as LWW
import VRDT.MultiSet (MultiSet(..), MultiSetOp(..))
import VRDT.TwoPMap (TwoPMap(..), TwoPMapOp(..))
import qualified VRDT.TwoPMap
import qualified VRDT.Types as VRDT
import Event.Types
type Widget t m a = (Reflex t, MonadHold t m, MonadFix m, Adjustable t m, NotReady t m, PostBuild t m, MonadNodeId m, TriggerEvent t m, PerformEvent t m, MonadIO (Performable m), PostBuild t m, MonadIO m, KyowonMonad m, KyowonMonad (Performable m)) => VtyWidget t m a
type State = TwoPMap UniqueId Event
type StateOp = TwoPMapOp UniqueId Event
-- State is TwoPMap of EventState
instance Eq EventOp
instance Ord EventOp
instance Aeson.ToJSON EventOp
instance Aeson.FromJSON EventOp
instance Aeson.ToJSON Event
instance Aeson.FromJSON Event
-- data EventOp =
-- EventTitleOp (Operation (LWWU Text))
-- | EventDescriptionOp (Operation (LWWU Text))
-- | EventStartTimeOp (Operation (LWWU UTCTime))
-- | EventEndTimeOp (Operation (LWWU UTCTime))
-- | EventLocationOp (Operation (LWWU Text))
-- deriving (Generic)
--
-- instance VRDT Event where
-- type Operation Event = EventOp
--
-- compatible (EventTitleOp op1) (EventTitleOp op2) = compatible op1 op2
-- compatible (EventDescriptionOp op1) (EventDescriptionOp op2) = compatible op1 op2
-- compatible (EventStartTimeOp op1) (EventStartTimeOp op2) = compatible op1 op2
-- compatible (EventEndTimeOp op1) (EventEndTimeOp op2) = compatible op1 op2
-- compatible (EventLocationOp op1) (EventLocationOp op2) = compatible op1 op2
-- compatible _ _ = True
--
-- apply e (EventTitleOp op) = e {eventTitle = apply (eventTitle e) op}
-- apply e (EventDescriptionOp op) = e {eventDescription = apply (eventDescription e) op}
-- apply e (EventStartTimeOp op) = e {eventStartTime = apply (eventStartTime e) op}
-- apply e (EventEndTimeOp op) = e {eventEndTime = apply (eventEndTime e) op}
-- apply e (EventLocationOp op) = e {eventLocation = apply (eventLocation e) op}
--
-- lawCommutativity e (EventTitleOp op1) (EventTitleOp op2) = lawCommutativity (eventTitle e) op1 op2
-- lawCommutativity e (EventDescriptionOp op1) (EventDescriptionOp op2) = lawCommutativity (eventDescription e) op1 op2
-- lawCommutativity e (EventStartTimeOp op1) (EventStartTimeOp op2) = lawCommutativity (eventStartTime e) op1 op2
-- lawCommutativity e (EventEndTimeOp op1) (EventEndTimeOp op2) = lawCommutativity (eventEndTime e) op1 op2
-- lawCommutativity e (EventLocationOp op1) (EventLocationOp op2) = lawCommutativity (eventLocation e) op1 op2
-- lawCommutativity _ _ _ = ()
--
-- lawCompatibilityCommutativity (EventTitleOp op1) (EventTitleOp op2) = lawCompatibilityCommutativity op1 op2
-- lawCompatibilityCommutativity (EventDescriptionOp op1) (EventDescriptionOp op2) = lawCompatibilityCommutativity op1 op2
-- lawCompatibilityCommutativity (EventStartTimeOp op1) (EventStartTimeOp op2) = lawCompatibilityCommutativity op1 op2
-- lawCompatibilityCommutativity (EventEndTimeOp op1) (EventEndTimeOp op2) = lawCompatibilityCommutativity op1 op2
-- lawCompatibilityCommutativity (EventLocationOp op1) (EventLocationOp op2) = lawCompatibilityCommutativity op1 op2
-- lawCompatibilityCommutativity _ _ = ()
-- TODO:
-- Make JSON instance.
-- Switch to EventId.
-- newtype EventId = EventId { unEventId :: UniqueId }
type EventId = UniqueId
-- instance Ord t => VRDT (LWW t a) where
-- type Operation (LWW t a) = LWW t a
-- enabled = LWW.enabledLWW
-- apply = LWW.applyLWW
main :: IO ()
main = do
-- TODO: Load these from the file system.
name <- do
args <- getArgs
case args of
[name] -> return $ Text.pack name
_ -> do
error "usage: event <display name>"
clientId <- createClient
let nextId = zeroNextId
mainWidget clientId nextId $ do
inp <- input
app name
return $ fforMaybe inp $ \case
V.EvKey (V.KChar 'c') [V.MCtrl] -> Just ()
_ -> Nothing
data View =
ViewEvents
| ViewCreateEvent
| ViewEvent EventId
| ViewEditEvent EventId Event
app :: Text -> Widget t m ()
app name = do
-- nav <- tabNavigation
-- runLayout (pure Orientation_Column) 0 nav $ do
clientId <- lift Reflex.getClientId
rec
st <- lift $ Reflex.connectToStore' storeRef initVRDT opsE
-- let eB = current e
-- let opsE = (undefined :: Reflex.Event (TwoPMapOp UTCTimestamp Event))
-- out :: Dynamic t (Reflex.Event t View, Reflex.Event t (TwoPMapOp UniqueId Event))
out <- networkHold (events st) $ ffor (switchDyn (fst <$> out)) $ \case
ViewCreateEvent -> createEvent clientId
ViewEvents -> events st
ViewEvent eId -> event name eId $ (Map.lookup eId . twoPMap) <$> st
ViewEditEvent eId e -> editEvent eId e
let opsE = switchDyn (snd <$> out)
return ()
where
storeRef = Reflex.StoreRef (Client.Server "http://localhost:3000") (Client.StoreId "TODO")
editEvent :: forall t m . EventId -> Event -> Widget t m (Reflex.Event t View, Reflex.Event t [StateOp])
editEvent eId event = do
nav <- tabNavigation
runLayout (pure Orientation_Column) 0 nav $ do
backE <- fixed 3 $ textButtonStatic def "Back"
title <- validateInput' "Title" Right id EventTitleOp (lwwValue $ eventTitle event)
description <- validateInput' "Description" Right id EventDescriptionOp (lwwValue $ eventDescription event)
startDate <- validateInput' "Start Date" dateValidation displayDate EventStartTimeOp (lwwValue $ eventStartTime event)
endDate <- validateInput' "End Date" dateValidation displayDate EventEndTimeOp (lwwValue $ eventEndTime event)
location <- validateInput' "Location" Right id EventLocationOp (lwwValue $ eventLocation event)
updateE <- fixed 3 $ textButtonStatic def "Update"
-- If all fields are valid, propogate updates.
let opsMD = (liftM5 . liftM5) (\a b c d e -> [a,b,c,d,e]) title description startDate endDate location
let opsE = Maybe.catMaybes <$> (catMaybes $ sampleOn updateE opsMD)
let viewE = leftmost [ ViewEvent eId <$ backE
, ViewEvent eId <$ opsE
]
return (viewE, opsE)
where
-- Validate input, check if the inputs changed, and create update operation.
validateInput' :: (Eq a) => Text -> (Text -> Either Text a) -> (a -> Text) -> (LWW UTCTimestamp a -> EventOp) -> a -> Layout t m (Dynamic t (Maybe (Maybe StateOp)))
validateInput' label validation render eventOp currentValue = do
t <- validateInput label validation $ Just $ render currentValue
clientId <- lift Reflex.getClientId
lift $ sampleMonotonicTimeWith (\a t -> (\v ->
-- Don't update if the value hasn't changed.
if v == currentValue then
Nothing
else
Just $ TwoPMapApply eId $ eventOp $ LWW (UTCTimestamp t clientId) v
) <$> a) t
event :: Text -> EventId -> Dynamic t (Maybe Event) -> Widget t m (Reflex.Event t View, Reflex.Event t [StateOp])
event name eId eventMD = do
nav <- tabNavigation
runLayout (pure Orientation_Column) 0 nav $ do
backE <- fixed 3 $ textButtonStatic def "Back"
widgetE' <- networkView $ ffor eventMD $ \case
Nothing -> do
fixed 1 $ text "Event does not exist."
return never
Just e -> do
editE <- fixed 3 $ textButtonStatic def "Edit"
fixed 1 $ text "Title:"
fixed 1 $ text $ pure $ lwwValue $ eventTitle e
fixed 1 $ text "Description:"
fixed 1 $ text $ pure $ lwwValue $ eventDescription e
fixed 1 $ text "Start time:"
fixed 1 $ text $ pure $ displayDate $ lwwValue $ eventStartTime e
fixed 1 $ text "End time:"
fixed 1 $ text $ pure $ displayDate $ lwwValue $ eventEndTime e
fixed 1 $ text "Location:"
fixed 1 $ text $ pure $ lwwValue $ eventLocation e
stateE <- fmap EventRSVPsOp <$> displayRSVPs name (eventRSVPs e)
return $ align (const e <$> editE) stateE
widgetE <- switchHold never widgetE'
let (editE, stateE) = fanThese widgetE
let viewE = leftmost [ ViewEvents <$ backE
, ViewEditEvent eId <$> editE
]
return (viewE, (pure . TwoPMapApply eId) <$> stateE)
where
displayRSVPs name (MultiSet posRSVPs negRSVPs) = do
fixed 1 $ text "RSVPs:"
let rsvps' = Map.toList posRSVPs
let nameCM = Map.lookup name negRSVPs
let rsvps = case nameCM of
Nothing -> rsvps'
Just 0 -> rsvps'
Just c -> (name, c):rsvps'
when (null rsvps) $
fixed 1 $ text "No RSVPs yet."
forM_ rsvps $ \(name, c) -> do
fixed 1 $ text $ pure $ name <> ": " <> Text.pack (show c)
-- Disable plus if c < 0
plusE <- if maybe False (< 0) nameCM then
return never
else
fixed 3 $ textButtonStatic def "+"
-- Disable minus if c <= 0
minusE <- if not (Map.member name posRSVPs) then
return never
else
fixed 3 $ textButtonStatic def "-"
return $ leftmost [
MultiSetOpAdd name 1 <$ plusE
, MultiSetOpRemove name 1 <$ minusE
]
events :: Dynamic t State -> Widget t m (Reflex.Event t View, Reflex.Event t [StateOp])
events st = do
nav <- tabNavigation
runLayout (pure Orientation_Column) 0 nav $ do
-- Create event button.
createE <- fixed 3 $ textButtonStatic def "Create an event"
-- Display events.
fixed 1 $ text $ pure "Events:"
selectEventE <- simpleList (Map.assocs . twoPMap <$> st) displayEvent
let view = leftmost
[ ViewCreateEvent <$ createE
, switchDyn (leftmost <$> selectEventE)
]
return (view, never)
where
displayEvent eventD = do
let eventText (_, e) = lwwValue (eventTitle e) <> " (" <> displayDate (lwwValue (eventStartTime e)) <> ")"
clickedE <- fixed 1 $ link $ current $ eventText <$> eventD
-- tile tileCfg $ do
-- -- TODO: Can we set background color when focused?
--
-- text $ current $ (lwwValue . eventTitle) <$> eventD
-- click <- void <$> mouseDown V.BLeft
-- let focusMe = leftmost [click] -- , sel, pb ]
-- return (focusMe, ())
-- Return selection event.
let viewEventE = ViewEvent . fst <$> tag (current eventD) clickedE
return viewEventE
-- tileCfg = TileConfig { _tileConfig_constraint = pure $ Constraint_Fixed 1
-- , _tileConfig_focusable = pure $ True
-- }
createEvent :: forall t m . ClientId -> Widget t m (Reflex.Event t View, Reflex.Event t [StateOp])
createEvent clientId = do
escapedE <- escapePressed
col $ do
rec
title <- validateInput "Title" Right Nothing >>= toLWW
description <- validateInput "Description" Right Nothing >>= toLWW
startDate <- validateInput "Start Date" dateValidation Nothing >>= toLWW
endDate <- validateInput "End Date" dateValidation Nothing >>= toLWW
location <- validateInput "Location" Right Nothing >>= toLWW
let rsvps = pure $ pure initVRDT
cancelE <- fixed 3 $ textButtonStatic def "Cancel"
createE <- fixed 3 $ textButtonStatic def "Create event"
let eventMD = (liftM6 . liftM6) Event title description startDate endDate location rsvps
let insertEventE = catMaybes $ sampleOn createE eventMD
insertE <- lift $ to2PMapInsert clientId insertEventE
let viewE = leftmost
[ ViewEvents <$ cancelE
, ViewEvents <$ escapedE
-- , ViewEvents <$ insertE
, (\(TwoPMapInsert k _) -> ViewEvent k) <$> insertE
]
return (viewE, pure <$> insertE)
where
toLWW :: Dynamic t (Maybe a) -> Layout t m (Dynamic t (Maybe (LWW UTCTimestamp a)))
toLWW = lift . Reflex.toLWW' clientId
to2PMapInsert clientId = nextIdWith $ \e nextId ->
let k = UniqueId clientId nextId in
TwoPMapInsert k e
validateInput :: (Reflex t, MonadNodeId m, MonadHold t m, MonadFix m) => Text -> (Text -> Either Text a) -> Maybe Text -> Layout t m (Dynamic t (Maybe a))
validateInput label validation initTextM = do
rec
let label' = addErr <$> current vE
fixed 1 $ text label'
let setInit = maybe id (\v c -> c {_textInputConfig_initialValue = Zipper.fromText v}) initTextM
t <- fixed 1 $ textInput $ setInit def
-- TODO: holdDyn on e
let vE = validation <$> _textInput_value t
return $ either (const Nothing) Just <$> vE
where
addErr (Left e) = label <> " (" <> e <> "):"
addErr _ = label <> ":"
dateValidation :: Text -> Either Text UTCTime
dateValidation = maybe (Left "Invalid date") Right . parseTimeM True defaultTimeLocale "%Y-%-m-%-d %l:%M%p" . Text.unpack
displayDate :: UTCTime -> Text
displayDate = Text.pack . formatTime defaultTimeLocale "%Y-%-m-%-d %l:%M%p"
sampleOn :: Reflex t => Reflex.Event t a -> Dynamic t b -> Reflex.Event t b
sampleOn event dyn = tag (current dyn) event
escapePressed :: (Reflex t, Monad m, HasVtyInput t m) => m (Reflex.Event t ())
escapePressed = do
i <- input
return $ fforMaybe i $ \case
V.EvKey V.KEsc [] -> Just ()
_ -> Nothing
-- app' :: (Reflex t, MonadHold t m, MonadFix m, Adjustable t m, NotReady t m, PostBuild t m, MonadNodeId m, TriggerEvent t m, PerformEvent t m, MonadIO (Performable m), PostBuild t m)
-- => ClientId -> UTCTime -> VtyWidget t m ()
-- app' clientId now = do
-- nav <- tabNavigation
-- runLayout (pure Orientation_Column) 0 nav $ do
-- rec
-- e <- lift $ Reflex.connectToStore storeRef e0 eOpE
-- let eB = current e
-- fixed 1 (text (lwwValue . eventTitle <$> eB))
-- fixed 1 (text (lwwValue . eventDescription <$> eB))
-- fixed 1 (text (lwwValue . eventLocation <$> eB))
--
-- locationE <- fixed 1 $ textInput $ def
--
-- eOpE <- (fmap EventLocationOp) <$> toLWW (updated (_textInput_value locationE))
--
-- -- buildE <- lift getPostBuild
-- -- let eOpE = (EventTitleOp $ l0 "Alice's birthday") <$ buildE
--
-- return ()
--
-- where
-- toLWW e = return $ l0 <$> e
--
--
-- storeRef = Reflex.StoreRef (Client.Server "http://localhost:3000") (Client.StoreId "TODO")
--
-- e0 = Event (l0 "Alice's birthday") (l0 "Let's celebrate Alice's birthday") (l0 now) (l0 now) (l0 "Someplace secret")
--
-- l0 :: a -> LWWU a
-- l0 = LWW (UTCTimestamp now clientId)
--
-- https://hackage.haskell.org/package/base-4.14.0.0/docs/src/GHC.Base.html#liftM5
liftM6 :: (Monad m) => (a1 -> a2 -> a3 -> a4 -> a5 -> a6 -> r) -> m a1 -> m a2 -> m a3 -> m a4 -> m a5 -> m a6 -> m r
liftM6 f m1 m2 m3 m4 m5 m6 = do { x1 <- m1; x2 <- m2; x3 <- m3; x4 <- m4; x5 <- m5; x6 <- m6; return (f x1 x2 x3 x4 x5 x6) }