datepicker

An fzf-like tool to interactively select a date in a provided format

git clone https://git.8pit.net/datepicker.git

  1module DatePicker.UI.Month (MonthView, mkMonthView) where
  2
  3import Data.Bool (bool)
  4import Data.List (find, findIndex)
  5import Data.List.NonEmpty qualified as NE
  6import Data.Maybe (fromJust)
  7import Data.Time.Calendar qualified as Cal
  8import Data.Time.Calendar.Month (Month, addMonths)
  9import Data.Time.LocalTime (LocalTime (LocalTime), TimeOfDay (TimeOfDay))
 10import DatePicker.UI (View (..))
 11import DatePicker.Util
 12import Graphics.Vty.Attributes qualified as Attr
 13import Graphics.Vty.Image qualified as I
 14import Graphics.Vty.Input.Events qualified as E
 15
 16data MonthView = MonthView
 17  { months :: [Month],
 18    curDay :: Cal.Day,
 19    numCols :: Int,
 20    movType :: Movement,
 21    weekOrd :: NE.NonEmpty Cal.DayOfWeek
 22  }
 23
 24instance View MonthView where
 25  draw = drawView
 26  process = processEvent
 27
 28mkMonthView :: [Month] -> Cal.Day -> Cal.DayOfWeek -> Bool -> MonthView
 29mkMonthView ms day firstWeekDay logicMove =
 30  let week = take 7 (enumFrom firstWeekDay)
 31      move = if logicMove then MLogical else MSpatial
 32   in MonthView ms day 3 move $ NE.fromList week
 33
 34currentMonth :: MonthView -> Month
 35currentMonth MonthView {months = ms, curDay = d} =
 36  fromJust $ find (\m -> Cal.dayPeriod d == m) ms
 37
 38hasDay :: MonthView -> Cal.Day -> Bool
 39hasDay MonthView {months = ms} d =
 40  any (\m -> Cal.dayPeriod d == m) ms
 41
 42hasMonth :: MonthView -> Month -> Bool
 43hasMonth MonthView {months = ms} m =
 44  m `elem` ms
 45
 46-- TODO: Make this customizable to implement the cal(1) -m option.
 47firstDayOfWeek :: MonthView -> Bool
 48firstDayOfWeek mv@MonthView {curDay = d, weekOrd = ord} =
 49  Cal.dayOfWeek d == NE.head ord
 50    || Cal.periodFirstDay (currentMonth mv) == d
 51
 52-- TODO: Make this customizable to implement the cal(1) -m option.
 53lastDayOfWeek :: MonthView -> Bool
 54lastDayOfWeek mv@MonthView {curDay = d, weekOrd = ord} =
 55  Cal.dayOfWeek d == NE.last ord
 56    || Cal.periodLastDay (currentMonth mv) == d
 57
 58firstWeekDayOfMonth :: MonthView -> Bool
 59firstWeekDayOfMonth mv@MonthView {curDay = day} =
 60  firstWeekDay (currentMonth mv) (Cal.dayOfWeek day) == day
 61  where
 62    firstWeekDay :: Month -> Cal.DayOfWeek -> Cal.Day
 63    firstWeekDay m dw = Cal.firstDayOfWeekOnAfter dw (Cal.periodFirstDay m)
 64
 65lastWeekDayOfMonth :: MonthView -> Bool
 66lastWeekDayOfMonth mv@MonthView {curDay = day} =
 67  Just day == lastWeekDay (currentMonth mv) (Cal.dayOfWeek day)
 68  where
 69    lastWeekDay :: Month -> Cal.DayOfWeek -> Maybe Cal.Day
 70    lastWeekDay m dw = find ((==) dw . Cal.dayOfWeek) $ reverse (Cal.periodAllDays m)
 71
 72------------------------------------------------------------------------
 73
 74drawView :: MonthView -> I.Image
 75drawView MonthView {curDay = d, months = ms, numCols = cols, weekOrd = ord} =
 76  I.vertCat (map I.horizCat $ splitEvery cols (map drawView' ms))
 77  where
 78    drawView' :: Month -> I.Image
 79    drawView' m =
 80      let img = drawMonth m ord d
 81       in img I.<|> makePad 2 (I.imageHeight img) I.<-> makePad weekWidth 1
 82
 83-- The return value specifies if the view has changed as a result
 84-- of processing the event, if so, 'drawView' needs to be invoked.
 85processEvent :: MonthView -> E.Event -> Either (Maybe MonthView) LocalTime
 86processEvent view@MonthView {curDay = day, movType = mov} (E.EvKey key _) =
 87  case key of
 88    E.KEnter -> Right $ LocalTime day (TimeOfDay 0 0 0)
 89    E.KUp -> Left $ moveCursor view mov PrevWeek
 90    E.KDown -> Left $ moveCursor view mov NextWeek
 91    E.KRight -> Left $ moveCursor view mov NextDay
 92    E.KLeft -> Left $ moveCursor view mov PrevDay
 93    _ -> Left Nothing
 94processEvent view (E.EvResize _ _) = Left $ Just view
 95processEvent _ _ = Left Nothing
 96
 97------------------------------------------------------------------------
 98
 99weekWidth :: Int
100weekWidth = (2 * 7) + 6 -- +6 for spacing between weeks
101
102drawDay :: Cal.Day -> Bool -> I.Image
103drawDay day curDay =
104  let attr = if curDay then high else Attr.defAttr
105   in I.string attr $ format "%_2e" day
106  where
107    high :: Attr.Attr
108    high =
109      Attr.defAttr
110        `Attr.withBackColor` Attr.white
111        `Attr.withForeColor` Attr.black
112
113drawWeeks :: Cal.Day -> NE.NonEmpty Week -> I.Image
114drawWeeks curDay weeks =
115  let w = NE.toList $ NE.map NE.toList weeks
116   in I.vertCat $ zipWith zipFunc [0 ..] (map drawWeek w)
117  where
118    zipFunc :: Int -> I.Image -> I.Image
119    zipFunc i = horizPad weekWidth (i == 0)
120
121    drawWeek :: [Cal.Day] -> I.Image
122    drawWeek days =
123      I.horizCat $
124        addSep (map (\day -> drawDay day $ day == curDay) days)
125
126-- XXX: Unfortunately, 'MonthYear' does not implement 'FormatTime'.
127drawMonthYear :: Month -> I.Image
128drawMonthYear m =
129  horizCenter weekWidth $
130    I.string Attr.defAttr (format "%B %Y" m)
131
132drawMonth :: Month -> NE.NonEmpty Cal.DayOfWeek -> Cal.Day -> I.Image
133drawMonth m ord curDay = drawMonthYear m I.<-> drawHeader ord I.<-> weeks
134  where
135    weeks :: I.Image
136    weeks = drawWeeks curDay (monthWeeks m $ NE.head ord)
137
138drawHeader :: NE.NonEmpty Cal.DayOfWeek -> I.Image
139drawHeader ord =
140  let wdays = NE.map (format "%a") ord
141      items = NE.map (I.string Attr.defAttr . shortenWeekDay) wdays
142   in I.horizCat $ addSep (NE.toList items)
143  where
144    shortenWeekDay :: String -> String
145    shortenWeekDay (f : s : _xs) = [f, s]
146    shortenWeekDay s = s
147
148------------------------------------------------------------------------
149
150data Direction = NextDay | PrevDay | NextWeek | PrevWeek
151  deriving (Eq, Show)
152
153data Movement = MLogical | MSpatial
154  deriving (Eq, Show)
155
156moveCursor :: MonthView -> Movement -> Direction -> Maybe MonthView
157moveCursor mv@MonthView {curDay = day, numCols = cols} mov dir
158  | mov == MSpatial && lastDayOfWeek mv && dir == NextDay =
159      moveSpatialHoriz mv 1 NE.head
160  | mov == MSpatial && firstDayOfWeek mv && dir == PrevDay =
161      moveSpatialHoriz mv (-1) NE.last
162  | mov == MSpatial && firstWeekDayOfMonth mv && dir == PrevWeek =
163      moveSpatialVert mv (cols * (-1)) NE.reverse
164  | mov == MSpatial && lastWeekDayOfMonth mv && dir == NextWeek =
165      moveSpatialVert mv cols id
166  | otherwise =
167      let newDay = moveLogical dir day
168       in bool Nothing (Just mv {curDay = newDay}) (hasDay mv newDay)
169
170moveLogical :: Direction -> Cal.Day -> Cal.Day
171moveLogical NextDay = Cal.addDays 1
172moveLogical PrevDay = Cal.addDays (-1)
173moveLogical NextWeek = addWeeks 1
174moveLogical PrevWeek = addWeeks (-1)
175
176moveSpatial ::
177  MonthView ->
178  Int ->
179  (Month -> Maybe Week) ->
180  (Week -> Maybe Cal.Day) ->
181  Maybe MonthView
182moveSpatial mv inc selectWeek selectDay =
183  let curMonth = currentMonth mv
184      newMonth = addMonths (fromIntegral inc) curMonth
185   in if hasMonth mv newMonth
186        then (\d -> mv {curDay = d}) <$> (selectWeek newMonth >>= selectDay)
187        else Nothing
188
189moveSpatialVert :: MonthView -> Int -> (Week -> Week) -> Maybe MonthView
190moveSpatialVert mv@MonthView {curDay = day} inc proc =
191  let dayOfWeek = Cal.dayOfWeek day
192   in moveSpatial
193        mv
194        inc
195        (Just . proc . NE.fromList . Cal.periodAllDays)
196        (find ((==) dayOfWeek . Cal.dayOfWeek))
197
198moveSpatialHoriz :: MonthView -> Int -> (Week -> Cal.Day) -> Maybe MonthView
199moveSpatialHoriz mv@MonthView {curDay = day, weekOrd = ord} inc select =
200  moveSpatial mv inc (\m -> monthWeeks m f !? weekOfMonth day) (Just . select)
201  where
202    f :: Cal.DayOfWeek
203    f = NE.head ord
204
205    weekOfMonth :: Cal.Day -> Int
206    weekOfMonth d =
207      let weeks = monthWeeks (Cal.dayPeriod d) f
208       in fromJust $ findIndex (elem d) (NE.toList weeks)
209
210{- ORMOLU_DISABLE -}
211-- From https://github.com/ghc/ghc/commit/d53f6f4d98aabd6f5b28fb110db1da0f6db70a06
212(!?) :: NE.NonEmpty a -> Int -> Maybe a
213xs !? n
214  | n < 0     = Nothing
215  | otherwise = foldr (\x r k -> case k of
216                                   0 -> Just x
217                                   _ -> r (k-1)) (const Nothing) xs n
218
219infixl 9 !?
220{- ORMOLU_ENABLE -}