#!/usr/bin/env stack {- stack script --resolver lts-19.23 --install-ghc -} {-# OPTIONS_GHC -Wall -Wno-unused-imports -Wno-unused-top-binds -Wno-name-shadowing #-} {-# language NoImplicitPrelude #-} {-# language RecordWildCards #-} {-# language FlexibleContexts #-} {-# language TemplateHaskell #-} {-# language ViewPatterns #-} {-# language OverloadedStrings #-} {-# language DeriveGeneric #-} {-# language DerivingVia #-} import Rebase.Prelude hiding (toList, on, (<+>), Max) import qualified Rebase.Prelude as Prelude import Control.Lens hiding ((<|)) import Data.Foldable (toList) import Data.Ratio import Text.Printf import Graphics.Vty import Data.Time.LocalTime import Control.Monad.RWS import Data.Time.Calendar.OrdinalDate import qualified Data.Text as Text import Data.Text.Format.Numbers import Rebase.Data.Map.Strict (Map) import qualified Rebase.Data.Map.Strict as Map import Brick import Brick.Types import Data.Text (unpack) import Control.Lens import Control.Monad (void, forever) import Control.Concurrent (threadDelay, forkIO) import qualified Graphics.Vty as V import Brick.Widgets.ProgressBar as P import Brick.BChan import Brick.Widgets.Center import Brick.Widgets.Border import Brick.Main ( App(..) , showFirstCursor , customMain , continue , halt ) import qualified Brick.Main as M import Brick.AttrMap ( attrMap ) import Brick.Types ( Widget , Next , EventM , BrickEvent(..) ) import Brick.Widgets.Core ( (<=>) , str ) import Brick.AttrMap as A import Brick.Util (fg, bg, on, clamp) import Brick.Widgets.Core import Brick.Widgets.Table import Text.Pretty.Simple (pPrint) import GHC.Generics (Generic) import Generic.Data import Generic.Data.Microsurgery data Performance = Achieved { achievedReps :: Integer, achievedWeight :: Rational } deriving Generic deriving (Read, Show) via Surgery Derecordify Performance data LiftRecord = LiftRecord { liftName :: Text, stats :: [Performance] } deriving Generic deriving (Read, Show) via Surgery Derecordify LiftRecord initial' :: [LiftRecord] initial' = [ LiftRecord "Squat" [Achieved 6 270, Achieved 8 255, Achieved 5 285, Achieved 10 255, Achieved 7 280], LiftRecord "Bench" [Achieved 7 190, Achieved 9 180, Achieved 4 205, Achieved 11 180, Achieved 8 200], LiftRecord "Deadlift" [Achieved 5 360, Achieved 9 315, Achieved 8 345, Achieved 7 360, Achieved 10 340], -- LiftRecord "Press" [Achieved 6 130, Achieved 9 120, Achieved 4 135] LiftRecord "Press" [Achieved 5 120], LiftRecord "Power Clean" [Achieved 10 135], LiftRecord "Push Press" [Achieved 7 135], LiftRecord "Front Squat" [Achieved 5 255], LiftRecord "Left-Arm Snatch" [Achieved 3 85], LiftRecord "Right-Arm Snatch" [Achieved 3 85], LiftRecord "Overhead Squat" [Achieved 5 65] ] computeRepGoal :: Rational -> [Performance] -> (Integer) computeRepGoal targetWeight stats = head $ filter isPR [2..] where isPR n = computeOneRepMax (Achieved n targetWeight) > computeOneRepMax (bestPerformance stats) bestPerformance :: [Performance] -> Performance bestPerformance = head . sortBy (flip $ comparing computeOneRepMax) -- The formula from Jim Wendler's 5-3-1: -- 1RM = Weight x Reps x 0.0333 + Weight. computeOneRepMax :: Performance -> Rational computeOneRepMax Achieved{..} = achievedWeight * (realToFrac achievedReps * 0.0333 + 1) showRational :: Rational -> String showRational n = printf format $ (realToFrac :: Rational -> Float) $ n where format = if floor (n * 10) `mod` 10 == (0 :: Integer) then "%.0f" else "%.2f" drawUI :: St -> [Widget ()] drawUI (St lifts _) = [vCenter $ vBox [hCenter oneRepMaxTable, withVScrollBarHandles $ withVScrollBars OnRight $ viewport () Vertical $ hCenter lastSetTable]] where lastSetTable = renderTable $ table $ map (padLeftRight 1 . str) ["Lift", "Week", "Last Set", "Goal", "Goal+1"] : concatMap toWeekRows lifts oneRepMaxTable = renderTable $ table $ map (padLeftRight 1 . str) ["Lift", "Achieved Best", "Computed 1RM"] : map toRow lifts toRow LiftRecord{..} = let best@Achieved{..} = bestPerformance stats in map (padLeftRight 2) [ txt $ liftName, str $ printf "%d @ %s" achievedReps (showRational achievedWeight), str $ showRational $ computeOneRepMax best ] toWeekRows lift = (flip map) [1,2,3::Int] $ \week -> toWeekRow week lift toWeekRow :: Int -> LiftRecord -> [Widget n] toWeekRow week LiftRecord{..} = let best = bestPerformance stats targetReps = case week of 1 -> 5; 2 -> 3; 3 -> 1; _ -> undefined :: Int targetPercentage = case week of 1 -> 85; 2 -> 90; 3 -> 95; _ -> undefined computedTarget = (* (targetPercentage % 100)) $ (* (90 % 100)) $ computeOneRepMax best targetWeight = ceilingN 5 computedTarget repGoal = computeRepGoal (targetWeight % 1) stats goalTo1RM g = computeOneRepMax $ Achieved g (targetWeight % 1) showGoal g = printf "%2d @ %d ≈ 1 @ %s" g targetWeight (showRational (goalTo1RM g)) in map (padLeftRight 2) [ txt $ (if week == 1 then liftName else " "), str $ show week, str $ printf "%2d+ @ %d" targetReps targetWeight, str $ showGoal repGoal, str $ showGoal (repGoal + 1) ] ceilingN :: Integer -> Rational -> Integer ceilingN n x = ceiling (x / toRational n) * n -- TODO: State contains chosen repmax formula -- TODO: State contains performances data St = St { lifts :: [LiftRecord], week :: WeekSelection } data WeekSelection = Week1 | Week2 | Week3 deriving Enum -- TODO: Event for inotify on edited text file (as input interface) data CustomEvent = CustomEvent handleEvent :: St -> BrickEvent () CustomEvent -> EventM () (Next St) handleEvent st e = case e of VtyEvent (V.EvKey V.KEsc []) -> halt st VtyEvent (V.EvKey V.KDown _) -> M.vScrollBy (M.viewportScroll ()) 5 >> continue st VtyEvent (V.EvKey V.KUp _) -> M.vScrollBy (M.viewportScroll ()) (-5) >> continue st VtyEvent _ -> continue st AppEvent _ -> continue st _ -> continue st theApp :: App St CustomEvent () theApp = App { appDraw = drawUI , appChooseCursor = showFirstCursor , appHandleEvent = handleEvent , appStartEvent = return , appAttrMap = const $ A.attrMap V.defAttr [] } -- Lift Ratios -- from https://www.catalystathletics.com/article/1786/The-Relation-of-Snatch-Clean-Jerk-and-Squat-Weights/ squat, deadlift, press :: Rational squat = 345 deadlift = 453 press = 140 cleanAndJerk = squat * 80 / 100 frontSquat = squat * 85 / 100 overheadSquat = squat * 65 / 100 snatch = squat * 60 / 100 powerSnatch = snatch * 80 / 100 clean = deadlift * 70 / 100 powerClean = clean * 80 / 100 pushPress = press * 100 / 75 jerk = pushPress / 100 * 85 main, main' :: IO () main' = pPrint initial' main = do let buildVty = V.mkVty V.defaultConfig vty <- buildVty -- liftIO $ setMode (outputIface vty) Mouse True chan <- newBChan 10 initial <- read <$> readFile "targets.dat" :: IO [LiftRecord] void $ customMain vty buildVty (Just chan) (theApp) (St initial Week1)