packages feed

cursedcsv-0.1: code/CursedCSV.hs

{-# LANGUAGE BangPatterns,
             FlexibleContexts,
             PatternGuards,
             ScopedTypeVariables
 #-}

module Main where

import Control.Applicative
import Control.Arrow
import Control.Exception(throwIO)
import Control.Monad.State.Strict
import Data.Char(isSpace)
import Data.ByteString.Char8(unpack,pack,ByteString)
import qualified Data.ByteString.Char8 as B
import Data.CSV.Enumerator
import qualified Data.Enumerator as E
import Data.Enumerator (($$))
import Data.Enumerator.Binary (enumHandle)
import Data.Function(on)
import Data.List(insertBy, intercalate, mapAccumL, sortBy, transpose, find)
import Data.Maybe(fromMaybe, isNothing)
import Data.Monoid
import Safe
import System.Exit
import System.IO
import Text.Regex.TDFA
import UI.HSCurses.Curses
import UI.HSCurses.CursesHelper
import qualified Algorithms.NaturalSort as NS
import System.Console.ParseArgs
import System.Posix.Signals

-- Util
--
{-
{-# NOINLINE myTrace #-}
myTrace msg x = unsafePerformIO $ appendFile "myTrace" (msg ++ "\n") >> return x
myTraceIt msg x = myTrace (msg++show x) x
-}

instance NS.NaturalSort ByteString where
    sortKey = NS.sortKey . unpack
    sortKeyCollated = (. unpack) . NS.sortKeyCollated

killSpace :: String -> String
killSpace = dropWhile isSpace . dropEndWhile isSpace
dropEndWhile :: (a -> Bool) -> [a] -> [a]
dropEndWhile p = foldr (\x xs -> if p x && null xs then [] else x:xs) []

-- Uses natural sort to handle numbers and strings.
--

type SortCol = (Int, Bool)
multiColSort :: NS.NaturalSort a => [SortCol] -> [[a]] -> [[a]]
multiColSort sCols rows = sortBy cmp rows
  where cmp r1 r2 = foldr f EQ sCols
          where f (idx, asc) dflt = case NS.compare <$> atMay r1' idx <*> atMay r2' idx of
                                      Nothing -> dflt
                                      Just EQ -> dflt
                                      Just ordering -> ordering
                  where (r1', r2') | asc = (r1, r2)
                                   | otherwise = (r2, r1)

replaceNth :: Int -> a -> [a] -> [a]
replaceNth n x xs = prev ++ x:next
  where (prev,next) = second (drop 1) $ splitAt n xs

removeNth :: Int ->  [a] -> [a]
removeNth n = uncurry (++) . second (drop 1) . splitAt n

updateSortCols :: SortCol -> [SortCol] -> [SortCol]
updateSortCols scol [] = [scol]
updateSortCols (col, asc) (sc@(c,_):cs)
    | col == c = (col, asc):cs
    | otherwise = sc : updateSortCols (col, asc) cs

deleteFirst :: (a -> Bool) -> [a] -> [a]
deleteFirst _ [] = []
deleteFirst predicate (c:cs)
    | predicate c = cs
    | otherwise = c : deleteFirst predicate cs

normTo0 :: Int -> Int
normTo0 x | x > 0     = x
          | otherwise = 0

-- Draw to curses screen
--
myScrSize :: IO (Int, Int)
myScrSize = (subtract 1 *** subtract 1) <$> scrSize

drawRows :: Int -> Int -> [[(String, Attr)]] -> IO ()
drawRows xOff width rows = do
    (hScr, wScr) <- myScrSize
--    myTrace ("drawRows: xStart="++show xStart++", yStart="++show yStart++", hScr="++show hScr++", wScr="++show wScr) $
    wMove stdScr 0 0
    let drawRow = go (min width wScr) . myDrop xOff
        myDrop _ [] = []
        myDrop n ((x,attr):xs)
            | n > len = myDrop (n - len) xs
            | n > 0 = (drop n x, attr) : xs
            | otherwise = (x, attr) : xs
          where len = length x
        go _ [] = myAddLn
        go w ((x,attr):xs)
            | w > 0 = do attrOn attr
                         drawLine len x
                         attrOff attr
                         go (w - len) xs
            | otherwise = myAddLn
          where len = min w (length x)
        myAddLn = do
            (y,_) <- getYX stdScr
            wMove stdScr (y + 1) 0
    mapM_ drawRow $ take hScr rows

drawStatus :: String -> IO ()
drawStatus str = do
  (h,w) <- myScrSize
  wMove stdScr h 0
  drawLine w (replicate w ' ')
  wMove stdScr h 0
  drawLine (length str) (str)

bye :: MonadIO m => m ()
bye = liftIO $ do
  _ <- cursSet CursorVisible
  refresh
  end
  exitWith ExitSuccess

-- Tables
--

-- Todo: give individual columns widths.
type ColIdent = (String, Int)

-- Todo: no space b4 first, but space between tbls
data Tbl a = Tbl {
      trows :: [[a]],
      tColsOrd:: [ColIdent],
      tx :: Int,  -- which character is at left of screen (assume cols of a row are flattened into one string for printing)
      ty :: Int,  -- which row is at top of screen
      colSelected :: Maybe Int, -- which column is selected
      tselected :: Maybe (Int,Int) -- which cell is selected; (x, y). NOTE: x coord is in cols, not chars
    } deriving Show
tcols :: Tbl a -> [String]
tcols = map fst . tColsOrd

initTbl :: Tbl a
initTbl = Tbl [] [] 0 0 Nothing Nothing

mkTbl :: [Int] -> [[ByteString]] -> Tbl ByteString
mkTbl idxs xs = case padIt xs of
                  (cls:rs) -> initTbl{trows = rs, tColsOrd = zip (map unpack cls) idxs}
                  _ -> initTbl

-- | pads all valuse in a col to be width of widest value in the column
padIt :: [[ByteString]] -> [[ByteString]]
padIt cls = transpose $ map padToMax $ transpose $ map (map (B.dropWhile isSpace)) cls --map (map fixNl) cls TODO
    where padToMax vs = map (padTo $ ml) vs  -- + 1 for column space
              where ml = maximum $ map B.length vs
          padTo l x = B.concat [B.singleton ' ', x, B.replicate (l - B.length x) ' ']

          fixNl [] = []
          fixNl ('\n':cs) = "\\n" ++ fixNl cs
          fixNl ('\t':cs) = "\\t" ++ fixNl cs
          fixNl (c:cs) = c : fixNl cs

-- | Given an index, returns the col at the index, and the table less that col.
getCol :: Int -> Tbl a -> ((ColIdent, [a]), Tbl a)
getCol i tbl = ((col, row), newTbl)
  where extractCol ix = go . splitAt ix
            where go (prev, x:rest) = (x, prev ++ rest)
                  go _ = error ("getCol "++show i)
        (col, cols) = extractCol i $ tColsOrd tbl
        (row, rows) = extractCol i $ transpose $ trows tbl
        cselNew | Just i == csel = Nothing
                | Just i < csel = subtract 1 <$> csel
                | otherwise = csel
          where csel = colSelected tbl
        tselNew | Just i == tselx = Nothing
                | Just i < tselx = second (subtract 1) <$> tsel
                | otherwise = tsel
          where tsel = tselected tbl
                tselx = fst <$> tsel
        newTbl = tbl{trows = transpose rows, tColsOrd = cols, colSelected = cselNew, tselected = tselNew}

-- | Given a column and a column identifier, insert it into the table

addCol :: (ColIdent, [a]) -> Tbl a -> Tbl a
addCol ((col, pos), vs) tbl = tbl{tColsOrd = cs, trows = rs}
  where cs = myAdd (col, pos) (tColsOrd tbl)
        rs = transpose $ map fst $ myAdd (vs, pos) $ flip zip (map snd cs) $ transpose $ trows tbl
        myAdd = insertBy (compare `on` snd)

-- test = (mkTbl [["c0","c1","c3"],["a0","a1","a2"],["b0","b1","b2"]]){tselected=Just(1,1), colSelected=Just 1}

-- Table manipulation
--

-- | returns idx of column which offSet is in,
--     previous cols (in reverse),
--     the offSet into this column,
--     the rest of the cols (including this one)
colIdx :: Int -> [[a]] -> (Int, [[a]], Int, [[a]])
colIdx offSet css = go 0 [] offSet css
  where go xc prevCols _ [] = (xc, prevCols, 0, [])
        go xc prevCols i (c:cs) | i' >= 0    = go (xc + 1) (c:prevCols) i' cs
                                | otherwise  = (xc, prevCols, i, c:cs)
          where i' = i - length c

-- | Scrolls table vertically by incr
tblVert :: Int -> Tbl a -> Tbl a
tblVert incr tbl = tbl {ty = ty tbl + incr}

-- | Scrolls table horizontally by incr
tblHoriz :: Int -> Tbl a -> Tbl a
tblHoriz incr tbl = tbl {tx = tx tbl + incr}

tblGotoLine :: Int -> Tbl a -> Tbl a
tblGotoLine ln tbl = tbl {ty = ln}

tblBot :: Tbl a -> Tbl a
tblBot tbl = tbl {ty = (length $ trows tbl) - 1}

-- | Scrolls table left by a column
tblLeftCol :: Tbl a -> Tbl a
tblLeftCol tbl | offSet > 0        = tblHoriz (negate offSet) tbl
               | (c:_) <- prevCols = tblHoriz (negate $ length c) tbl
               | otherwise         = tbl
  where (_cIdx, prevCols, offSet, _cs) = colIdx (tx tbl) (tcols tbl)

-- | Scrolls table right by a column
tblRightCol :: Tbl a -> Tbl a
tblRightCol tbl | (c:_) <- cs = tblHoriz (length c - offSet) tbl
                | otherwise   = tbl
  where (_cIdx, _prevCols, offSet, cs) = colIdx (tx tbl) (tcols tbl)

-- | Make table wider
growTbl :: (Int, a) -> (Int, a)
growTbl (w, tbl) = (w + 1, tbl)

-- | Make table narrower
shrinkTbl :: (Int, a) -> (Int, a)
shrinkTbl (w, tbl) | w > 1 = (w - 1, tbl)
                   | otherwise = (w, tbl)

-- | Given a target, a size, and an offset, then return how far to scroll.
adjVisible :: Int -> Int -> Int -> Int
adjVisible targ sz offSet | offSet <= targ && targ <= offSet + sz = 0
                          | offSet > targ = targ - offSet
                          | otherwise = targ - (offSet + sz)

rowAdjTbl :: Int -> Int -> Tbl a -> Int
rowAdjTbl y h tbl = adjVisible y h (ty tbl)

colAdjTbl :: Int -> Int -> Tbl a -> Int
colAdjTbl i w (tbl@Tbl{tx=xOff}) = adjVisible targ w xOff
  where cs = tcols tbl
        (prev,rest) = splitAt i cs
        ix0 = sum $ map length prev
        ix1 = ix0 + sum [length x | x <- take 1 rest]
        targ | ix1 > xOff + w = ix1
             | otherwise = ix0

-- | Move the column selection by an increment. If no col is selected, pick first.
moveColSelected :: Int -> (Int, Tbl ByteString) -> (Int, Tbl ByteString)
moveColSelected n (w, tbl@(Tbl{colSelected=colSel})) = (w, newTbl)
  where newCol = case colSel of
                   Nothing -> 0
                   Just cIdx -> if cIdx + n < length cs && cIdx + n >= 0
                                then cIdx + n
                                else cIdx
        newTbl = tblHoriz (colAdjTbl newCol w tbl) tbl{colSelected=Just newCol}
        cs = tcols tbl


-- turn a table into a list of rows with attributes for each cell
-- takes height and width of table and does cropping
--
drawTbl :: Bool -> Bool -> Int -> Int -> Tbl ByteString -> [[(String, Attr)]]
drawTbl isActive _showSort height width (tbl@Tbl{trows=rs, tx=xPos, ty=yPos, colSelected=cSel, tselected=tsel}) =
   activeMark : map padRow (cols:rows)
  where
    cs = tcols tbl
    activeMark = [(take width $ mark:repeat ' ', convertAttributes [])]
      where mark | isActive = '*'
                 | otherwise = ' '

    (_,cols) = mapAccumL doCol (0, width) cs
    doCol (idx, w) c = ((idx + 1, w - length c), (c, convertAttributes attrs))
      where attrs | Just idx == cSel = [Reverse]
                  | otherwise        = []

    -- do height cropping
    xsel = fst <$> tsel
    ysel = snd <$> tsel
    height' = height - 3  -- (if showSort then height - 1 else height) - 3
    rows = drop (min yPos $ max 0 $ numRows - height') rows'
    (numRows, rows') = mapAccumL doRow 0 $ map (map unpack) rs
    doRow yIdx r = (yIdx + 1, snd $ mapAccumL go 0 r)
      where boldRow = Just yIdx == ysel
            go xIdx v = (xIdx + 1, (v, convertAttributes attrs))
              where attrs | boldRow && (Just xIdx == xsel) = [Bold]
                          | otherwise                      = []

    -- do width cropping
    dropX = 1 + min xPos (max 0 $ (length $ concat cs) - width)  -- if width is bigger than actual tbl then no movement. We always drop at least one to correct for initial space.
    padRow = takeRow width . dropRow dropX
      where dropRow _ [] = []
            dropRow cnt ((v, attr):vs) | cnt > vLen = dropRow (cnt - vLen) vs
                                       | cnt > 0 = (drop cnt v, attr) : vs
                                       | otherwise = (v, attr):vs
              where vLen = length v
            takeRow cnt [] = [(replicate cnt ' ', convertAttributes [])]
            takeRow cnt ((v, attr):vs) | cnt > vLen = (v, attr) : takeRow (cnt - vLen) vs
                                       | cnt > 0 = [(take cnt v, attr)]
                                       | otherwise = []
              where vLen = length v


-- | turn a list of widths and table into a list rows with attributes for each cell.
-- Then draws all rows and the status line to screen.
drawTbls :: CsvM ()
drawTbls = do
    AppState{xApp=xOff, myTbls=tbls, tblCur=curTbl, sortCols=scs, statusLine=stl} <- get
    liftIO $ do
      (hScr, wScr) <- liftIO myScrSize
      let allTbls = zipWith (\(w,t) i -> drawTbl (i == curTbl) (not $ null scs) hScr w t) tbls [0..]
          allRows = foldr1 zipTbls allTbls
      erase
      drawRows xOff wScr allRows
      maybe (drawStatus "") drawStatus stl
      refresh
  where
    zipTbls (x:xs) (y:ys) = (x ++ ("|",noAttr) : y) : go xs ys
        where go (a:as) (b:bs) = (a ++ (" ",noAttr) : b) : go as bs
              go as [] = as
              go [] bs = bs
    zipTbls xs [] = xs
    zipTbls [] ys = ys
    noAttr = convertAttributes []
{-
    addSort | showSort = ([(concat $ map fmtSort sCols, convertAttributes [])] :)
            | otherwise = id
      where fmt x = [(x, convertAttributes [])]
    fmtSort (idx, asc) | asc = colNm++"^ "
                       | otherwise = colNm++"v "
      where colNm = Msc.killSpace (atNote ("fmt "++show idx) cs idx) ++ " "
-}


-- Finding
--
myFind :: (a -> Maybe b) -> [a] -> Maybe b
myFind p = getFirst . go
  where go [] = First Nothing
        go (x:xs) = First (p x) `mappend` go xs

findInList :: Show elm => Bool -> Bool -> Maybe (elm -> Bool) -> (elm -> Maybe idx) -> [elm] -> Maybe idx
findInList useMatchElm backwards brkFunOpt predicate xs = myFind predicate row
  where (prevElms, nextElms) | Just bf <- brkFunOpt = first reverse $ break bf xs
                             | backwards                = (reverse xs, [])
                             | otherwise                = ([], xs)

        row | backwards && useMatchElm           = take 1 nextElms ++ prevElms
            | backwards                          = prevElms
            | useMatchElm || isNothing brkFunOpt = nextElms
            | otherwise                          = drop 1 nextElms

matchFun :: RegexLike regex source => regex -> (source, a) -> Maybe a
matchFun regex (x,i) | match regex x = Just i
                     | otherwise     = Nothing

brkFun :: Eq a => Maybe a -> Maybe ((b,a) -> Bool)
brkFun x = (\i (_,y) -> y == i) <$> x


findTbl :: Bool -> Maybe Regex -> Tbl ByteString -> Maybe (Int, Int)
findTbl backwards regex (Tbl{trows = rs, tselected = tsel}) =
    findInList True backwards (brkFun $ snd <$> tsel) rowPred (zip rs [0..])
  where
    rowPred (r,i) = flip (,) i <$> findInList False backwards (xBrkFun $ fst <$> tsel) mFun (zip r [0..])
      where xBrkFun xsel | Just i == (snd <$> tsel) = brkFun xsel
                         | otherwise = Nothing
    mFun = maybe (const Nothing) matchFun regex



-- Main state
--

data AppState = AppState {
      myTbls :: [(Int, Tbl ByteString)], -- width, table
      tblCur :: Int,
      myCols :: [String],
      myRows :: [[ByteString]],
      sortCols :: [(Int,Bool)], -- sorting preference for cols and whether col is ascending (we have a multi col sort)
      rxHist :: [String],
      rxCur :: Maybe Regex,
      xApp :: Int,
      statusLine :: Maybe String
    }

type CsvM a = StateT AppState IO a

putStatus :: Maybe String -> CsvM ()
putStatus s = modify (\as -> as {statusLine = s})

--Reproject rows into underlying tables. Needed when the order is changed
projectViews :: CsvM ()
projectViews = do
    as@AppState{myTbls=nums:myVs, myRows=rs, myCols=_cs, sortCols=_srtCs} <- get
    let proj tbl = tbl{trows = map doProj rs}
          where cIdxs = map snd $ tColsOrd tbl
                doProj r = map (atNote "projectViews" r) cIdxs
    put as{myTbls = nums:map (second proj) myVs}

withCurTbl :: ((Int, Tbl ByteString) -> CsvM a) -> CsvM a
withCurTbl f = do
    as <- get
    let tbl = atNote "withCurTbl" (myTbls as) (tblCur as)
    f tbl

putCurTbl :: (Int, Tbl ByteString) -> CsvM ()
putCurTbl tbl = modify $ \as -> as{myTbls = replaceNth (tblCur as) tbl (myTbls as)}

modCurTbl :: Int -> ((Int, Tbl ByteString) -> (Int, Tbl ByteString)) -> CsvM ()
modCurTbl h f = withCurTbl $ \tbl -> putCurTbl (correct h $ f tbl)

correct :: Int -> (Int, Tbl a) -> (Int, Tbl a)
correct h (w, t@(Tbl{tx=x, ty=y, trows=rs})) = (w, t{tx = x', ty = y'})
  where
    x' = max 0 $ min (length (concat cs) - w) x
    y' = max 0 $ min ((length rs - 1) - h) y
    cs = tcols t

correctAs :: AppState -> AppState
correctAs as = as {tblCur = newTblCur}
    where
      newTblCur = max 1 $ min (length (myTbls as) - 1) $ tblCur as

modAllTbls :: Int -> (Tbl ByteString -> Tbl ByteString) -> CsvM ()
modAllTbls h f = do
    as <- get
    put as{myTbls = map (correct h . second f) (myTbls as)}

curTblLeft, curTblRight :: CsvM ()
curTblLeft  = modify $ \as -> correctAs $ as {tblCur = tblCur as - 1}
curTblRight = modify $ \as -> correctAs $ as {tblCur = tblCur as + 1}

tryAdjColWidths :: Int -> Int -> Int -> (Int, Int)
tryAdjColWidths decrW incrW delta = (decrW - delta', incrW + delta')
    where
      decrDeltaDiff = decrW - delta
      delta'
        | decrDeltaDiff < 5 = delta - (decrDeltaDiff - 5)
        | otherwise = delta

moveColLeft :: (Int, Tbl ByteString) -> CsvM ()
moveColLeft (_, Tbl{colSelected=Nothing}) = return ()
moveColLeft (w, tbl@Tbl{colSelected=Just csel}) = do
    as @ AppState{myTbls=tbls, tblCur=curTblIdx} <- get
    let ((nw, newTbl), newTblIdx, newCurTblIdx)
            | curTblIdx > 1 = (atNote "moveColLeft" tbls (curTblIdx - 1), curTblIdx - 1, curTblIdx)
            | otherwise = ((1, (mkTbl [] []) {ty=ty tbl}), 1, curTblIdx + 1)
        (curCol, tbl') = getCol csel tbl
        newTbl' = addCol curCol newTbl
        (w',nw') = tryAdjColWidths w nw $ length (fst $ fst curCol)
        newTbls
            | curTblIdx > 1 = replaceNth newCurTblIdx (w', tbl') $ replaceNth newTblIdx (nw', newTbl') tbls
            | otherwise = replaceNth newCurTblIdx (w', tbl') $ myCons (nw', newTbl') tbls
        myCons x = uncurry (++) . second (x :) . splitAt 1
    put $ correctAs $ as{myTbls = filter ((> 0) . length . tColsOrd . snd) newTbls, tblCur = newCurTblIdx}

moveColRight :: (Int, Tbl ByteString) -> CsvM ()
moveColRight (_, Tbl{colSelected=Nothing}) = return ()
moveColRight (w, tbl@Tbl{colSelected=Just csel}) = do
    as @ AppState{myTbls=tbls, tblCur=curTblIdx} <- get
    let tblsLen = length tbls - 1
        (nw, newTbl)
            | curTblIdx < tblsLen = atNote "moveColRight" tbls (curTblIdx + 1)
            | otherwise = (1, (mkTbl [] []) {ty=ty tbl})
        (curCol, tbl') = getCol csel tbl
        newTbl' = addCol curCol newTbl
        (w',nw') = tryAdjColWidths w nw $ length (fst $ fst curCol)
        newTbls
            | curTblIdx < tblsLen = replaceNth curTblIdx (w', tbl') $ replaceNth (curTblIdx + 1) (nw', newTbl') tbls
            | otherwise = replaceNth curTblIdx (w', tbl') $ tbls ++ [(nw', newTbl')]
    put $ correctAs $ as{myTbls = filter ((> 0) . length . tColsOrd . snd) newTbls}

addSortCol :: Bool -> (Int, Tbl ByteString) -> CsvM ()
addSortCol _ (_, Tbl{colSelected=Nothing}) = return ()
addSortCol asc (_, Tbl{trows=_rs, colSelected=Just csel}) = do
    scs <- updateSortCols (csel, asc) . sortCols <$> get
    modify $ \as -> as{sortCols = scs, myRows = multiColSort scs (myRows as)}
    projectViews

remSortCol :: (Int, Tbl ByteString) -> CsvM ()
remSortCol (_, Tbl{colSelected=Nothing}) = return ()
remSortCol (_, Tbl{trows=_rs, colSelected=Just csel}) = do
    scs <- deleteFirst ((== csel) . fst) . sortCols <$> get
    modify $ \as -> as{sortCols = scs, myRows = multiColSort scs (myRows as)}
    projectViews

showSortOrder :: CsvM ()
showSortOrder = do
  sCols <- sortCols <$> get
  cls    <- myCols <$> get
  putStatus $ Just $ "Sort Ordering: " ++ intercalate ", " (map (fmtSort cls) sCols)
        where fmtSort cs (idx, asc) | asc = colNm++" ^ "
                                    | otherwise = colNm++" v"
                  where colNm = killSpace (atNote ("fmt "++show idx) cs idx)


-- User Input
--
getK :: CsvM Key
getK = getK' (return ())

getK' :: CsvM () -> CsvM Key
getK' f = getKey $ do
            liftIO endWin
            liftIO refresh
            (h, w) <- liftIO scrSize
            as <- get
            let width = sum [x | (x,_) <- myTbls as] - xApp as
            modCurTbl h $ \(wTbl, tbl) -> (max 2 (wTbl + w - width), tbl)
            drawTbls
            f
            liftIO refresh

getLn :: String -> CsvM String
getLn msg = do
  liftIO $ drawStatus $ msg
  liftIO $ refresh
  go 0 ""
  where go hstN str = do
          c <- getK' $ liftIO $ drawStatus (msg ++ str)
          -- (_, w) <- liftIO myScrSize
          let getHistory incr = do
                hst <- ("" :) . rxHist <$> get
                let hstN1 = incr hstN
                    hstN2 | hstN1 >= length hst = 0
                          | hstN1 < 0 = length hst - 1
                          | otherwise = hstN1
                    str' = atNote "getLn"  hst hstN2
                -- (y,_x) <- liftIO $ getYX stdScr
                liftIO $ drawStatus (msg ++ str') >> refresh
                go hstN2 str'
          case c of
            KeyChar '\r' -> return str
            KeyBackspace -> do
              -- (y,_x) <- liftIO $ getYX stdScr
              let str' = (reverse . drop 1 . reverse) str
              liftIO $ drawStatus (msg ++ str') >> refresh
              go hstN str'
            KeyChar char -> do
              liftIO $ wAddStr stdScr [char]
              liftIO $ refresh
              go hstN $ str ++ [char]
            KeyUp -> getHistory (+ 1) -- do
            KeyDown -> getHistory (subtract 1) -- do
            _ -> go hstN str

queryStatus :: String -> CsvM a -> CsvM a -> CsvM a
queryStatus msg ycont ncont = do
  str <- getLn (msg ++ " (y/n): ")
  case str of
    ('y':_) -> ycont
    _ -> ncont

runFindCol :: Bool -> Maybe Regex -> (Int, Tbl ByteString) -> CsvM ()
runFindCol backwards regex (w, tbl) =
    case findInList False backwards (brkFun $ colSelected tbl) (maybe (const Nothing) matchFun $ regex) (zip (tcols tbl) [0..]) of
      Just cIdx -> putCurTbl (w, (tblHoriz (colAdjTbl cIdx w tbl) tbl){colSelected=Just cIdx})
      Nothing -> if backwards
                 then queryStatus "Not found. Start from end?"
                                  (runFindCol True regex (w, tbl{colSelected=Nothing}))
                                  (return ())
                 else queryStatus "Not found. Start from beginning?"
                                  (runFindCol False regex (w, tbl{colSelected=Nothing}))
                                  (return ())

runFind :: Int -> Bool -> Maybe Regex -> (Int, Tbl ByteString) -> CsvM ()
runFind h backwards regex (w, tbl) =
    case findTbl backwards regex tbl of
      Just pos@(xPos, yPos) -> do let yDelta = rowAdjTbl yPos h tbl
                                      xDelta = colAdjTbl xPos w tbl
                                  modCurTbl h $ \(w', t) -> (w', tblHoriz xDelta t{tselected = Just pos})
                                  modAllTbls h $ tblVert yDelta
      Nothing -> if backwards
                 then queryStatus "Not found. Start from end?"
                                  (runFind h True regex (w, tbl{tselected=Nothing}))
                                  (return ())
                 else queryStatus "Not found. Start from beginning?"
                                  (runFind h False regex (w, tbl{tselected=Nothing}))
                                  (return ())

unselectVal, unselectCol :: Tbl a -> Tbl a
unselectVal tbl = tbl{tselected=Nothing}
unselectCol tbl = tbl{colSelected=Nothing}

--todo pass in h
incrSearch :: String -> (Bool -> Maybe Regex -> (Int, Tbl ByteString) -> CsvM ()) -> (Tbl ByteString -> Tbl ByteString) -> CsvM ()
incrSearch prompt findFun unselectFun = do
  lnp <- getLn prompt
  (h,_) <- liftIO myScrSize
  case killSpace lnp of
    "" -> modCurTbl h (second unselectFun) >> modify (\as -> as {rxCur = Nothing})
    ln -> do
      let regex = Just $ makeRegex ln
      modify (\as -> as {rxHist = ln : rxHist as, rxCur = regex})
      withCurTbl $ findFun False regex

gotoLine :: Int -> CsvM ()
gotoLine h = do
  ln <- getLn "goto line: "
  case readMay ln of
    Just lnNum -> modAllTbls h (tblGotoLine lnNum)
    Nothing -> putStatus $ Just "Bad line number."

-- REPL
--
processKeys :: (Int -> Int -> Maybe Regex -> KeyTree) -> CsvM ()
processKeys kt = go
    where
      go = do
        drawTbls
        putStatus Nothing
        key <- getK
        as <- get
        (h,w) <- liftIO myScrSize
        let regex = rxCur as
        fromMaybe (putStatus (Just $ "Did not recognize key: " ++ showKey key)) $
                  lookupKey key (kt (h - 3) w regex) -- nb (h - 2) ignores a possible sortcol. counts status line and tbl select, figure out what to do?
        if key `elem` map KeyChar ['e','q']
          then return ()
          else go

type KeyAssoc = ([Key], CsvM (), String)
type KeyTree = [(String, [KeyAssoc])]

lookupKey :: Key -> KeyTree -> Maybe (CsvM ())
lookupKey k kt = (\(_,x,_) -> x) <$> find (\(ks,_,_) -> k `elem` ks) km
 where km = concat . map snd $ kt

showKey :: Key -> String
showKey kc = case kc of
     KeyChar ' ' -> "Spc"
     KeyUp -> "Up"
     KeyDown -> "Down"
     KeyLeft -> "Left"
     KeyRight -> "Right"
     KeySLeft -> "Shift-Left"
     KeySRight -> "Shift-Right"
     KeyPPage -> "PgUp"
     KeyNPage -> "PgDown"
     KeyUnknown 565 -> "^Up"
     KeyUnknown 521 -> "^Down"
     KeyUnknown 559 -> "^Right"
     KeyUnknown 541 -> "^Left"
     KeyUnknown 560 -> "^Shift-Right"
     KeyUnknown 542 -> "^Shift-Left"
     KeyChar '\t' -> "Tab"
     KeyChar '\SUB' -> "^z"
     KeyChar '\ETX' -> "^c"
     KeyBTab -> "Shift-Tab"
     KeyChar x -> show x
     x -> show x

keyTreeNoHelp :: Int -> Int -> Maybe Regex -> KeyTree
keyTreeNoHelp h w regex = ("",[([KeyChar 'h',KeyChar '?'], return (), "")]) : keyTree h w regex

suspendMe :: CsvM ()
suspendMe = liftIO $ raiseSignal sigTSTP

keyTree :: Int -> Int -> Maybe Regex -> KeyTree
keyTree h w regex = [
     ("Basics",
      [ ([KeyChar 'h', KeyChar '?'], liftIO helpScreen, "Display this help")
      , ([KeyChar 'e', KeyChar 'q'], return (), "Exit")
      , ([KeyChar 'r'], liftIO $ erase >> refresh, "Redraw")
      , ([KeyChar '\SUB'], suspendMe, "Suspend")
      -- , ([KeyChar '\ETX'], bye, "Force Exit")
      ])

   , ("Moving",
      [ ([KeyDown], modAllTbls h (tblDown 1), "Forward one line")
      , ([KeyUp], modAllTbls h (tblUp 1), "Backward one line")
      , ([KeyChar ' ', KeyNPage, KeyUnknown 521], modAllTbls h (tblDown (h - 1)), "Forward one page")
      , ([KeyChar 'b', KeyPPage, KeyUnknown 565], modAllTbls h (tblUp (h - 1)), "Backward one page")
      , ([KeyRight]  , modCurTbl h (tblRight 1), "Right one character")
      , ([KeyLeft]   , modCurTbl h (tblLeft 1), "Left one character")
      , ([KeySRight] , modCurTbl h (second tblRightCol), "Right one column")
      , ([KeySLeft]  , modCurTbl h (second tblLeftCol), "Left one column")
      , ([KeyUnknown 559] , modCurTbl h (tblRight (w - 1)), "Right one page")
      , ([KeyUnknown 541] , modCurTbl h (tblLeft (w - 1)), "Left one page")
      ])

   , ("Jumping",
      [ ([KeyChar 'g', KeyHome], modAllTbls h (tblGotoLine 0), "Go to first line")
      , ([KeyChar 'G', KeyEnd], modAllTbls h tblBot, "Go to last line")
      , ([KeyChar '\a'], gotoLine h, "Jump to a given line")
      -- To add: goto line.
      ])

   , ("Searching",
      [ ([KeyChar '/'], incrSearch "/" (runFind h) unselectVal, "Enter regular expression and search rows for a match")
      , ([KeyChar 'n'], withCurTbl (runFind h False regex), "Repeat previous search on rows")
      , ([KeyChar 'p'], withCurTbl (runFind h True  regex), "Repeat previous search on rows in reverse direction")
      , ([KeyChar ':'], incrSearch ":" (runFindCol) unselectVal, "Enter regular expression and search column names for a match")
      , ([KeyChar 'N'], withCurTbl (runFindCol False regex), "Repeat previous search on rcolumn names")
      , ([KeyChar 'P'], withCurTbl (runFindCol True  regex), "Repeat previous search on column names in reverse direction")
      ])

   , ("Column Navigation",
      [ ([KeyChar '\t'], modCurTbl h (moveColSelected 1), "Move column selection to the right")
      , ([KeyBTab], modCurTbl h (moveColSelected (-1)), "Move column selection to the left")
      ])

   , ("Sorting",
      [ ([KeyChar '^'], withCurTbl (addSortCol True), "Make selected column ascending in sort, or add it to sort")
      , ([KeyChar 'v'], withCurTbl (addSortCol False), "Make selected column descending in sort, or add it to sort")
      , ([KeyChar 'x'], withCurTbl remSortCol, "Remove column from sort")
      , ([KeyChar '&'], showSortOrder, "Display current sort ordering")
      ])

   , ("Slicing",
      [ ([KeyUnknown 542], curTblLeft, "Move table focus left")
      , ([KeyUnknown 560], curTblRight, "Move table focus right")
      , ([KeyChar '<'], withCurTbl moveColLeft, "Move selected column left one table")
      , ([KeyChar '>'], withCurTbl moveColRight, "Move selected column right one table")
      , ([KeyChar 'a'], modCurTbl h growTbl, "Grow current table 1 column")
      , ([KeyChar 'z'], modCurTbl h shrinkTbl, "Shrink current table 1 column")
      ])

   ]
  where
    tblUp = tblVert . negate
    tblDown = tblVert
    tblLeft = second . tblHoriz . negate
    tblRight = second . tblHoriz
{-
    moveApp incr = modify go
      where go as = as{xApp = min (max 0 $ len - w) (max 0 (xApp as + incr))}
              where len = sum $ map fst $ myTbls as
-}

helpScreen :: IO ()
helpScreen = do
  (_, wScr) <- myScrSize
  evalStateT (processKeys keyTreeNoHelp) $ AppState
                 [(0, mkTbl [] []), (wScr, tbl)] 1 (tcols tbl) (trows tbl) [] [] Nothing 0 Nothing
      where tbl = mkTbl [0..] $ map (map pack) $ ["keys","action"] : concatMap render (keyTree 0 0 Nothing)
            render (heading,kas) =
                ["","---------"] :
                [""," * " ++ heading ++ " * "] :
                ["",""] :
                map (\(ks,_,desc) -> [intercalate " " (map showKey ks), desc]) kas

--todo: catch exception and cleanup



main :: IO ()
main = do
  pargs <- parseArgsIO ArgsComplete
          [
           Arg "Separator" (Just 's') (Just "separator") (argDataDefaulted "character" ArgtypeString ",") "separator character, defaults to comma",
           Arg "Help" (Just 'h') (Just "help") Nothing "Displays this help",
           Arg "Infile"  Nothing Nothing (argDataOptional "filename"  ArgtypeString) "Input file"
          ]
  when (gotArg pargs "Help") $ usageError pargs ""
  let sepChar = fromMaybe ',' $ join $ fmap headMay $ getArg pargs "Separator"
      foldCSVHandle h csvs f acc = E.run (enumHandle 4096 h $$ iterCSV csvs f acc)
      readCSVHandle s h = fmap reverse <$> foldCSVHandle h s collectRows []

  inHandle <- getArgStdio pargs "Infile" ReadMode
  canRead <- hReady inHandle
  when (not canRead) $ usageError pargs ""
  mydata <- {-fmap (map (map unpack))$ -} either throwIO return =<< readCSVHandle (defCSVSettings {csvSep = sepChar}) inHandle
  start
  -- [headsty,tbsty] <- convertStyles [AttributeStyle [Bold] DarkGreenF BlackB, Style DarkGreenF DefaultB]
  (_h, w) <- myScrSize
  let myTbl = mkTbl [0..] mydata
      nums = (5, mkTbl [0] (map (map pack) $ ["row"] : map ((:[]) . show) [(0::Int) .. length mydata]))
      as = AppState [nums, (w - 5, myTbl)] 1 (tcols myTbl) (trows myTbl) [] [] Nothing 0 Nothing
  evalStateT (processKeys keyTree) $ as
  bye