datepicker

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

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

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