1{-# LANGUAGE OverloadedLists #-}23module DatePicker.UI.Time (TimeView, mkTimeView) where45import Data.Char (digitToInt, isDigit)6import Data.List.NonEmpty qualified as NE7import Data.Time.Calendar qualified as Cal8import Data.Time.LocalTime (LocalTime (LocalTime), TimeOfDay (todHour, todMin), makeTimeOfDayValid)9import DatePicker.UI (View (..))10import DatePicker.Util (format, horizCenter, makePad)11import Graphics.Vty.Attributes qualified as Attr12import Graphics.Vty.Image qualified as I13import Graphics.Vty.Input.Events qualified as E1415data TimeView = TimeView16 { rawInput :: NE.NonEmpty Int,17 position :: Word,18 initTime :: LocalTime19 }2021instance View TimeView where22 draw = drawView23 process = processEvent2425type ClockGlyph = [[Int]]2627-- Shamelessly stolen from the tmux clock-mode.28--29-- See: https://github.com/tmux/tmux/blob/3.5a/window-clock.c#L53-L12430clockFont :: [ClockGlyph]31clockFont =32 [ [ [1, 1, 1, 1, 1], -- 033 [1, 0, 0, 0, 1],34 [1, 0, 0, 0, 1],35 [1, 0, 0, 0, 1],36 [1, 1, 1, 1, 1]37 ],38 [ [0, 0, 0, 0, 1], -- 139 [0, 0, 0, 0, 1],40 [0, 0, 0, 0, 1],41 [0, 0, 0, 0, 1],42 [0, 0, 0, 0, 1]43 ],44 [ [1, 1, 1, 1, 1], -- 245 [0, 0, 0, 0, 1],46 [1, 1, 1, 1, 1],47 [1, 0, 0, 0, 0],48 [1, 1, 1, 1, 1]49 ],50 [ [1, 1, 1, 1, 1], -- 351 [0, 0, 0, 0, 1],52 [1, 1, 1, 1, 1],53 [0, 0, 0, 0, 1],54 [1, 1, 1, 1, 1]55 ],56 [ [1, 0, 0, 0, 1], -- 457 [1, 0, 0, 0, 1],58 [1, 1, 1, 1, 1],59 [0, 0, 0, 0, 1],60 [0, 0, 0, 0, 1]61 ],62 [ [1, 1, 1, 1, 1], -- 563 [1, 0, 0, 0, 0],64 [1, 1, 1, 1, 1],65 [0, 0, 0, 0, 1],66 [1, 1, 1, 1, 1]67 ],68 [ [1, 1, 1, 1, 1], -- 669 [1, 0, 0, 0, 0],70 [1, 1, 1, 1, 1],71 [1, 0, 0, 0, 1],72 [1, 1, 1, 1, 1]73 ],74 [ [1, 1, 1, 1, 1], -- 775 [0, 0, 0, 0, 1],76 [0, 0, 0, 0, 1],77 [0, 0, 0, 0, 1],78 [0, 0, 0, 0, 1]79 ],80 [ [1, 1, 1, 1, 1], -- 881 [1, 0, 0, 0, 1],82 [1, 1, 1, 1, 1],83 [1, 0, 0, 0, 1],84 [1, 1, 1, 1, 1]85 ],86 [ [1, 1, 1, 1, 1], -- 987 [1, 0, 0, 0, 1],88 [1, 1, 1, 1, 1],89 [0, 0, 0, 0, 1],90 [1, 1, 1, 1, 1]91 ],92 [ [0, 0, 0, 0, 0], -- :93 [0, 0, 1, 0, 0],94 [0, 0, 0, 0, 0],95 [0, 0, 1, 0, 0],96 [0, 0, 0, 0, 0]97 ]98 ]99100mkTimeView :: TimeOfDay -> LocalTime -> TimeView101mkTimeView cur = TimeView (toInput cur) 0102 where103 toInput :: TimeOfDay -> NE.NonEmpty Int104 toInput t = fromInt (todHour t) `NE.append` fromInt (todMin t)105106 fromInt :: Int -> NE.NonEmpty Int107 fromInt n =108 let t = map digitToInt $ show n109 in if length t < 2 then 0 NE.:| t else NE.fromList t110111drawView :: TimeView -> I.Image112drawView v@TimeView {initTime = t} =113 let str = format "%-d %B, %Y" t114 clk = drawClock v115 in horizCenter (I.imageWidth clk) (I.string Attr.defAttr str)116 I.<-> makePad (I.imageWidth clk) 1117 I.<-> drawClock v118119processEvent :: TimeView -> E.Event -> Either (Maybe TimeView) LocalTime120processEvent view (E.EvKey key _mods) =121 case key of122 E.KChar c -> Left $ processInput view c123 E.KBS -> Left $ Just (moveCursor view (-1))124 E.KLeft -> Left $ Just (moveCursor view (-1))125 E.KRight -> Left $ Just (moveCursor view 1)126 E.KEnter -> case getTimeOfDay view of127 Nothing -> Left Nothing -- TODO: Provide visual feedback128 Just t -> Right $ LocalTime (Cal.ModifiedJulianDay 0) t129 _ -> Left Nothing130processEvent view (E.EvResize _ _) = Left $ Just view131processEvent _ _ = Left Nothing132133------------------------------------------------------------------------134135moveCursor :: TimeView -> Int -> TimeView136moveCursor view@TimeView {rawInput = input, position = p} off =137 let len = fromIntegral $ NE.length input138 in view {position = (p + fromIntegral off) `mod` len}139140drawGlyph :: ClockGlyph -> Attr.Attr -> I.Image141drawGlyph glyph attr =142 let digits = map (`drawBlock` attr) glyph143 in I.vertCat digits I.<|> makePad 1 (length digits)144145drawClock :: TimeView -> I.Image146drawClock TimeView {position = curPos, rawInput = input} =147 let (h, m) = NE.splitAt 2 $ NE.zipWith drawDigit [0 ..] input148 in I.horizCat h I.<|> colonSep I.<|> I.horizCat m149 where150 defAttr :: Attr.Attr151 defAttr = Attr.defAttr `Attr.withBackColor` Attr.cyan152153 drawDigit idx digit =154 drawGlyph (clockFont !! digit) $155 if idx == curPos156 then defAttr `Attr.withBackColor` Attr.magenta157 else defAttr158159 colonSep :: I.Image160 colonSep = drawGlyph (last clockFont) defAttr161162drawBlock :: [Int] -> Attr.Attr -> I.Image163drawBlock blk attr = I.horizCat $ map (\i -> I.char (a i) ' ') blk164 where165 a i = if i == 1 then attr else Attr.defAttr166167getTimeOfDay :: TimeView -> Maybe TimeOfDay168getTimeOfDay TimeView {rawInput = input} =169 let (h, m) = NE.splitAt 2 input170 in makeTimeOfDayValid (toInt h) (toInt m) 0171 where172 toInt :: [Int] -> Int173 toInt = read . concatMap show174175processInput :: TimeView -> Char -> Maybe TimeView176processInput v c177 | isDigit c = Just $ cycleDigits v (digitToInt c)178 | otherwise = Nothing179180cycleDigits :: TimeView -> Int -> TimeView181cycleDigits v@TimeView {position = p, rawInput = input} n =182 let newView = moveCursor v 1183 in newView {rawInput = newInput}184 where185 newInput :: NE.NonEmpty Int186 newInput = NE.zipWith (\e i -> if i == p then n else e) input [0 ..]