dbmigrations-0.1: src/StoreManager.hs
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 ++ " <store-path>")
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