module Main where import Control.Exception import Control.Monad.State import Data.Maybe ( fromJust, isNothing ) import qualified Data.Map as Map import System.Environment (getArgs, getProgName) import System.Exit (exitFailure) import UI.HSCurses.Widgets import qualified UI.HSCurses.Curses as Curses import qualified UI.HSCurses.CursesHelper as CursesH import Database.Schema.Migrations.Filesystem import Database.Schema.Migrations.Store title :: String title = "Migration Manager" help :: String help = "q:quit" data MMState = MMState { mmStoreData :: StoreData , mmStatus :: String , mmStyles :: [CursesH.CursesStyle] , mmStorePath :: FilePath } type MM = StateT MMState IO type ToplineWidget = TextWidget type HelpLineWidget = TextWidget type StatusBarWidget = TableWidget type MigrationListWidget = TableWidget data MainWidget = MainWidget { toplineWidget :: ToplineWidget , helplineWidget :: HelpLineWidget , statusbarWidget :: StatusBarWidget , migrationListWidget :: MigrationListWidget } instance Widget MainWidget where draw pos sz hint w = draw pos sz hint (mkRealMainWidget (Just sz) w) minSize w = minSize (mkRealMainWidget Nothing w) runMM :: FilePath -> StoreData -> [CursesH.CursesStyle] -> MM a -> IO a runMM sp storeData cstyles mm = evalStateT mm (MMState { mmStoreData = storeData , mmStyles = cstyles , mmStatus = sp ++ " loaded." , mmStorePath = sp }) getSize :: MM (Int, Int) getSize = liftIO $ Curses.scrSize styles :: [CursesH.Style] styles = [ CursesH.defaultStyle , CursesH.AttributeStyle [CursesH.Bold] CursesH.GreenF CursesH.DarkBlueB ] nthStyle :: Int -> MM CursesH.CursesStyle nthStyle n = do cs <- gets mmStyles return $ cs !! n defStyle :: MM CursesH.CursesStyle defStyle = nthStyle 0 lineStyle :: MM CursesH.CursesStyle lineStyle = nthStyle 1 lineDrawingStyle :: MM DrawingStyle lineDrawingStyle = do s <- lineStyle return $ mkDrawingStyle s lineOptions :: MM TextWidgetOptions lineOptions = do sz <- getSize ds <- lineDrawingStyle return $ TWOptions { twopt_size = TWSizeFixed (1, getWidth sz) , twopt_style = ds , twopt_halign = AlignLeft } mkToplineWidget :: MM ToplineWidget mkToplineWidget = do opts <- lineOptions return $ newTextWidget (opts { twopt_halign = AlignCenter }) title mkHelpLineWidget :: MM HelpLineWidget mkHelpLineWidget = do opts <- lineOptions return $ newTextWidget opts help -- We need to insert a dummy widget at the lower-right corner of the -- window, i.e. at the lower-right corner of the message -- line. Otherwise, an error occurs because drawing a character to -- this position moves the cursor to the next line, which doesn't -- exist. mkStatusBarWidget :: MM StatusBarWidget mkStatusBarWidget = do sz <- getSize msg <- gets mmStatus let width = getWidth sz opts = TWOptions { twopt_size = TWSizeFixed (1, width - 1) , twopt_style = defaultDrawingStyle , twopt_halign = AlignLeft } tw = newTextWidget opts msg row = [TableCell tw, TableCell $ EmptyWidget (1,1)] tabOpts = defaultTBWOptions { tbwopt_minSize = (1, width) } return $ newTableWidget tabOpts [row] -- nlines = height of status line + height of help line + height of -- top line nlines :: Int nlines = 3 migrationListHeight :: (Int, Int) -> Int migrationListHeight (h, _) = h - nlines migrationListOptions :: MM TableWidgetOptions migrationListOptions = do sz <- getSize return $ TBWOptions { tbwopt_fillCol = Nothing , tbwopt_fillRow = None , tbwopt_activeCols = [0] , tbwopt_minSize = (migrationListHeight sz, getWidth sz) } mkMigrationListWidget :: MM MigrationListWidget mkMigrationListWidget = do storeData <- gets mmStoreData sz <- getSize let rows = map (migrationRow $ getWidth sz) (Map.keys $ storeDataMapping storeData) opts <- migrationListOptions return $ newTableWidget opts rows where migrationRow w s = [TableCell $ newTextWidget (defaultTWOptions { twopt_size = TWSizeFixed (1, w) }) s] validPos :: Pos -> TableWidget -> Bool validPos pos w = (getWidth pos) `elem` (tbwopt_activeCols $ tbw_options w) && (getHeight pos) < (length $ tbw_rows w) && (getHeight pos) >= 0 && (getWidth pos) >= 0 moveUp :: TableWidget -> TableWidget moveUp orig = if isNothing (tbw_pos orig) then orig else let oldPos = fromJust $ tbw_pos orig newPos = ((getHeight $ oldPos) - 1, getWidth oldPos) in if validPos newPos orig then orig { tbw_pos = Just newPos } else orig moveDown :: TableWidget -> TableWidget moveDown orig = if isNothing (tbw_pos orig) then orig else let oldPos = fromJust $ tbw_pos orig newPos = ((getHeight $ oldPos) + 1, getWidth oldPos) in if validPos newPos orig then orig { tbw_pos = Just newPos } else orig mkMainWidget :: MM MainWidget mkMainWidget = do tlw <- mkToplineWidget clw <- mkMigrationListWidget blw <- mkHelpLineWidget msglw <- mkStatusBarWidget return $ MainWidget tlw blw msglw clw mkRealMainWidget :: Maybe Size -> MainWidget -> TableWidget mkRealMainWidget msz w = let cells = [ TableCell $ toplineWidget w , TableCell $ migrationListWidget w , TableCell $ helplineWidget w , TableCell $ statusbarWidget w ] rows = map singletonRow cells opts = case msz of Nothing -> defaultTBWOptions Just sz -> defaultTBWOptions { tbwopt_minSize = sz } in newTableWidget opts rows updateStateDependentWidgets :: MainWidget -> MM MainWidget updateStateDependentWidgets w = do statusbar <- mkStatusBarWidget -- update the message line with the -- state's status return $ w { statusbarWidget = statusbar } updateStatus :: String -> MM () updateStatus msg = do st <- get put st { mmStatus = msg } move :: Direction -> MainWidget -> MM MainWidget move dir w = do let listWidget = case dir of DirUp -> moveUp orig DirDown -> moveDown orig _ -> orig orig = migrationListWidget w w' <- updateStateDependentWidgets w return $ w' { migrationListWidget = listWidget } resize :: Widget w => MM w -> MM () resize f = do liftIO $ do Curses.endWin Curses.resetParams Curses.cursSet Curses.CursorInvisible Curses.refresh w <- f redraw w redraw :: Widget w => w -> MM () redraw w = do sz <- getSize liftIO $ do draw (0, 0) sz DHNormal w Curses.refresh eventloop :: MainWidget -> MM () eventloop w = do k <- CursesH.getKey (resize mkMainWidget) case k of Curses.KeyChar 'q' -> return () Curses.KeyUp -> process $ move DirUp w Curses.KeyDown -> process $ move DirDown w _ -> eventloop w where process f = do w' <- f redraw w' eventloop w' mmMain :: MM () mmMain = do w <- mkMainWidget redraw w eventloop w main :: IO () main = do args <- getArgs let theStorePath = args !! 0 storeData <- if length args /= 1 then do p <- getProgName putStrLn ("Usage: " ++ p ++ " ") exitFailure else do let store = FSStore { storePath = theStorePath } result <- loadMigrations store case result of Left es -> do putStrLn "There were errors in the migration store:" forM_ es $ \err -> do putStrLn $ " " ++ show err exitFailure Right theStoreData -> return theStoreData runCurses theStorePath storeData `finally` CursesH.end where runCurses sp storeData = do CursesH.start cstyles <- CursesH.convertStyles styles Curses.cursSet Curses.CursorInvisible runMM sp storeData cstyles mmMain