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