mywork-1.0.0.0: app/Panes/AddProj.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}
-- KWQ: adding a project *just* with a location could potentially query the (local) location for the details.
-- KWQ: if adding a project and capable of scanning for details (cabal file, etc.), what if determined project name is different than current project name (i.e. adding a bad location)?
-- KWQ: with more VCS sophistication, it could compare different locations (esp to canonical location) to determine if ahead/behind/etc.
-- KWQ: location dates as the latest date of any file touched in that location (this could be a big scan! Limit depth to 1? Just dirs?)
-- KWQ: locationstatus git recognition could add branch info for local location?
-- KWQ: global "find location"
-- KWQ: adding a Pull Request as a new location (F5 context-specific)
-- KWQ: TODO notes
-- KWQ: ON_DATE notes
-- KWQ: notes should show location/source. Only MyWorkDB notes should be editable.
-- KWQ: no notes for git/darcs relationships... part of location instead?
-- KWQ: changing dir on exit doesn't work (because it's the tui sub-proc that changes dir), and it probably shouldn't without explicit direction, but can we echo to stdout better?
-- KWQ: group heirarchy: work/ARCOS/RACK
-- KWQ: Multi-language projects (e.g. RACK is Java, Python, Prolog, SADL, ...
-- KWQ: should full note be a separate pane? yes?
-- KWQ; filebrowser helper for location
-- KWQ: check remote VCS
-- KWQ: location for hackage
-- KWQ: check hackage status/date
-- KWQ: pull requests notification
-- KWQ: build failures notification
-- KWQ: indication in ProjInfo of which VCS (Git, Darcs, etc.).
-- KWQ: if submodules, sync gets locations and create projects/locations for those submodules? How to relate to the main project?
-- KWQ: Projects list differentiation: Work, Personal, other [filtering? visual grouping? what sorting for the Projects list?]
-- KWQ: sorting for all panes. (and filtering)
-- KWQ: Notes could be two lists with a scrollable edit window between, associated with the selected list item. This makes scrolling hard though, because need to intercept event to split between two lists. However, how bad is it if there is just a single list and the list render function just draws one entry extra long? However, another bad effect here is that a very long note can consume the list display area, making scrolling very awkward (similar to MH with large embedded messages). Maybe the separate area really is the best visual design...
-- KWQ: NAmedStr, SayText, Seq, etc.
module Panes.AddProj
(
AddProjPane
, initAddProj
, isAddProjActive
, projectInputResults
)
where
import Brick hiding ( Location )
import Brick.Focus
import Brick.Forms
import Brick.Panes
import qualified Brick.Widgets.Center as C
import qualified Brick.Widgets.Core as BC
import qualified Brick.Widgets.Table as BT
import Control.Lens hiding ( under )
import Data.Either ( isRight )
import qualified Data.List as DL
import Data.Maybe ( isJust )
import qualified Data.Sequence as Seq
import Data.Text ( Text )
import qualified Data.Text as T
import Data.Time.Calendar ( Day )
import qualified Graphics.Vty as Vty
import Defs
import Panes.Common.Attrs
import Panes.Common.Inputs
import Sync
data AddProjPane
data NewProj = NewProj { _npName :: ProjectName
, _npRole :: Role
, _npGroupG :: Maybe Group
, _npGroupT :: Text
, _npLangR :: Either Text Language
, _npLangT :: Text
, _npDesc :: Text
, _npLoc :: LocationSpec
, _npLocDate :: Maybe Day
}
makeLenses ''NewProj
blankNewProj :: NewProj
blankNewProj = NewProj (ProjectName "") User (Just Personal) "" (Right C)
"" "" (RemoteSpec "") Nothing
type ProjForm = Form NewProj MyWorkEvent WName
instance Pane WName MyWorkEvent AddProjPane where
data (PaneState AddProjPane MyWorkEvent) = NP { nPF :: Maybe ProjForm
-- Just == pane active
, nPrj :: Maybe Project
-- reset to Nothing when nPF
-- transitions Nothing -> Just
, nOrig :: Maybe Project
, nErr :: Maybe Text
}
type (EventType AddProjPane WName MyWorkEvent) = BrickEvent WName MyWorkEvent
initPaneState _ = NP Nothing Nothing Nothing Nothing
drawPane ps _gs =
C.centerLayer
. modalB ((maybe "New" (const "Edit") $ nOrig ps) <> " Project")
. vLimitPercent 80
. hLimitPercent 65
. (\f -> vBox [ -- withVScrollBars OnRight
-- $ viewport (WName "AddProjForm:viewport") Vertical
-- $
renderForm f
, padBottom (Pad 1) $ withAttr a'Error
$ maybe (txt " ") txt (nErr ps)
, emptyWidget
, vLimit 1 (fill ' ' <+> str "Ctrl-D = accept"
<+> fill ' ' <+> str "ESC = abort"
<+> fill ' ')
]) <$> nPF ps
focusable _ ps = case nPF ps of
Nothing -> mempty
Just f -> Seq.fromList $ focusRingToList $ formFocus f
handlePaneEvent _ = \case
VtyEvent (Vty.EvKey Vty.KEsc []) -> nPFL %%~ const (return Nothing)
VtyEvent (Vty.EvKey (Vty.KChar 'd') [Vty.MCtrl]) -> \s ->
let pf = s ^. nPFL
np form = Project { name = form ^. npName
, group = case form ^. npGroupG of
Just r -> r
Nothing -> OtherGroup $ form ^. npGroupT
, role = form ^. npRole
, language = case form ^. npLangR of
r@(Right _) -> r
Left _ -> Left $ form ^. npLangT
, description = form ^. npDesc
, locations =
case form ^. npLoc of
RemoteSpec rs | T.null rs -> mempty
_ -> [ Location
{ location = form ^. npLoc
, locatedOn = form ^. npLocDate
, locValid = True -- assumed
, notes = mempty
}
]
}
in if maybe False allFieldsValid pf
then do let p0 = np . formState <$> pf
p <- case p0 of
Nothing -> return Nothing
Just jp -> Just <$> syncProject jp
return $ s & nPFL .~ Nothing & newProject .~ p
else
let badflds = maybe "none"
(foldr (\n a -> if T.null a
then T.pack n
else T.pack n <> ", " <> a) ""
. fmap show . invalidFields)
pf
errmsg = "Correct invalid entries before accepting: "
in return $ s { nErr = Just $ errmsg <> badflds }
ev -> \s -> validateForm
$ s { nErr = Nothing }
& (nPFL . _Just %%~ \w -> nestEventM' w (handleFormEvent ev))
nPFL :: Lens' (PaneState AddProjPane MyWorkEvent) (Maybe ProjForm)
nPFL f s = (\n -> s { nPF = n }) <$> f (nPF s)
isAddProjActive :: PaneState AddProjPane MyWorkEvent -> Bool
isAddProjActive = isJust . nPF
newProject :: Lens' (PaneState AddProjPane MyWorkEvent) (Maybe Project)
newProject f s = (\n -> s { nPrj = n}) <$> f (nPrj s)
-- | Returns the original project name (if any) and the new Project
-- specification.
projectInputResults :: PaneState AddProjPane MyWorkEvent
-> (Maybe ProjectName, Maybe Project)
projectInputResults ps = (name <$> nOrig ps, nPrj ps)
validateForm :: EventM WName es (PaneState AddProjPane MyWorkEvent)
-> EventM WName es (PaneState AddProjPane MyWorkEvent)
validateForm inner = do
s <- inner
case s ^. nPFL of
Nothing -> return s
Just pf -> do
let isOK1 = or [ formState pf ^. npGroupG /= Nothing
, formState pf ^. npGroupT /= ""
]
let tgtfld1 = WName "Other Group Text"
let isOK2 = or [ isRight (formState pf ^. npLangR)
, formState pf ^. npLangT /= ""
]
let tgtfld2 = WName "Other Language Name"
(ltgt, lvalid) <- validateLocationInput True $ formState pf ^. npLoc
return $ s
& nPFL %~ fmap (setFieldValid isOK1 tgtfld1)
& nPFL %~ fmap (setFieldValid isOK2 tgtfld2)
& nPFL %~ fmap (setFieldValid lvalid ltgt)
initAddProj :: Projects
-> Maybe Project
-> PaneState AddProjPane MyWorkEvent
-> PaneState AddProjPane MyWorkEvent
initAddProj prjs mbProj ps =
case nPF ps of
Just _ -> ps
Nothing ->
let label s = padBottom (Pad 1) . label' s
label' s w = (vLimit 1 $ hLimit labelWidth
$ fill ' ' <+> str s <+> str ": ") <+> w
under s w = padBottom (Pad 1)
$ vLimit 1
$ padLeft (Pad (labelWidth + 4))
$ str s <+> w
labelWidth = 18
numCols lastSolo nc =
let go wdgs =
if null wdgs then []
else fmap padded (DL.take nc (wdgs <> DL.repeat emptyWidget))
: go (DL.drop nc wdgs)
padded = padRight (BC.Pad 2)
renderT = BT.renderTable
. BT.surroundingBorder False
. BT.rowBorders False
. BT.columnBorders False
in if lastSolo
then \wdgs ->
if null wdgs then emptyWidget
else (renderT $ BT.table $ go $ DL.init wdgs)
<=> DL.last wdgs
else renderT . BT.table . go
projFields =
[ label "Project name" @@=
let validate = \case
[] -> Nothing
[""] -> Nothing
(nmt:_) -> let nm = ProjectName nmt
in if nm `elem` (name <$> projects prjs) &&
(maybe True ((nm /=) . name) mbProj)
then Nothing -- invalid
else Just nm
in editField npName (WName "New Project Name") (Just 1)
(\(ProjectName nm) -> nm) validate (txt . headText) id
, label' "Group" @@=
radioField npGroupG
[ (Just Personal, (WName "+Prj:Grp:Personal"), "Personal")
, (Just Work, (WName "+Prj:Grp:Work"), "Work")
, (Nothing, (WName "Other Group Text"), "Other")
]
, under "...: " @@=
editTextField npGroupT (WName "+Proj:Grp:Text") (Just 1)
, label "Role" @@=
-- setFieldConcat (hBox . DL.intersperse (str " ")) .
setFieldConcat (numCols False 2)
. radioField npRole
[ (Author, (WName "+Prj:Role:Author"), "Author")
, (Maintainer, (WName "+Prj:Role:Maintainer"), "Maintainer")
, (Contributor, (WName "+Prj:Role:Contributor"), "Contributor")
, (User, (WName "+Prj:Role:User"), "User")
]
, label' "Language" @@=
setFieldConcat (numCols True 4)
. radioField npLangR
[ (Right C, (WName "+Prj:Lang:C"), "C")
, (Right CPlusPlus, (WName "+Prj:Lang:CPP"), "C++")
, (Right Haskell, (WName "+Prj:Lang:Haskell"), "Haskell")
, (Right JavaScript, (WName "+Prj:Lang:JS"), "JavaScript")
, (Right Prolog, (WName "+Prj:Lang:Prolog"), "Prolog")
, (Right Python, (WName "+Prj:Lang:Python"), "Python")
, (Right Rust, (WName "+Prj:Lang:Rust"), "Rust")
, (Left "", (WName "Other Language Name"), "Custom")
]
, under "...: " @@=
editTextField npLangT (WName "+Prj:Lang:Text") (Just 1)
, label "Description" @@=
editTextField npDesc (WName "+Prj:Desc") Nothing
]
locFields =
case mbProj of
Nothing ->
-- Only query for the initial location if this is a new project;
-- do not query for an existing project.
[
label "Initial location" @@=
locationInput mempty Nothing True npLoc
, label "Location date" @@= mbDateInput npLocDate
]
_ -> []
npForm =
newForm (projFields <> locFields)
(case mbProj of
Nothing -> blankNewProj
Just p -> NewProj { _npName = name p
, _npRole = role p
, _npGroupG = case group p of
Personal -> Just Personal
Work -> Just Work
OtherGroup _ -> Nothing
, _npGroupT = case group p of
OtherGroup t -> t
_ -> ""
, _npLangR = language p
, _npLangT = case language p of
Right _ -> ""
Left t -> t
, _npDesc = description p
, _npLoc = RemoteSpec ""
, _npLocDate = Nothing
}
)
in NP { nPF = Just npForm
, nPrj = Nothing
, nOrig = mbProj
, nErr = Nothing
}