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