From c1b1fddb764d785e3ef8a7962beb9686323bdbbb Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 27 Sep 2018 23:39:22 +0000 Subject: [PATCH 01/28] Load tasks on startup again --- app/Main.hs | 3 ++- src/TaskMachine/UI.hs | 21 +++++++++++++++++++-- src/TaskMachine/UI/NewTask.hs | 8 -------- src/TaskMachine/UI/TopBar.hs | 6 ------ todo.txt | 2 +- 5 files changed, 22 insertions(+), 18 deletions(-) delete mode 100644 src/TaskMachine/UI/NewTask.hs delete mode 100644 src/TaskMachine/UI/TopBar.hs diff --git a/app/Main.hs b/app/Main.hs index 493fc17..e67a0d9 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -10,4 +10,5 @@ import TaskMachine.UI main :: IO() main = do o <- parseOptions - void $ B.defaultMain myApp (startUIState o) + state <- loadTasks $ startUIState o + void $ B.defaultMain myApp state diff --git a/src/TaskMachine/UI.hs b/src/TaskMachine/UI.hs index 2609c41..9fddcc5 100644 --- a/src/TaskMachine/UI.hs +++ b/src/TaskMachine/UI.hs @@ -1,6 +1,7 @@ module TaskMachine.UI ( myApp , startUIState + , loadTasks ) where import qualified Brick as B @@ -9,15 +10,20 @@ import qualified Brick.Themes as B import qualified Data.Vector as V import qualified Graphics.Vty.Input.Events as VTY +import TaskMachine.LTask import TaskMachine.Options -import TaskMachine.UI.NewTask import TaskMachine.UI.Popup import TaskMachine.UI.TaskList -import TaskMachine.UI.TopBar import TaskMachine.UI.Types {- Rendering -} +placeholderTopBar :: B.Widget n +placeholderTopBar = B.str "Prune | Reload | Search: " B.<+> B.vLimit 1 (B.fill '_') + +placeholderNewTask :: B.Widget RName +placeholderNewTask = B.str "New: " B.<+> B.vLimit 1 (B.fill '_') + drawBaseLayer :: UIState -> B.Widget RName drawBaseLayer s = B.vBox [placeholderTopBar, renderTaskList True (tasks s), placeholderNewTask] @@ -25,6 +31,17 @@ drawUIState :: UIState -> [B.Widget RName] drawUIState s@UIState{errorPopup=Just p} = [renderPopupOk p, drawBaseLayer s] drawUIState s = [drawBaseLayer s] +{- Actions -} + +loadTasks :: UIState -> IO UIState +loadTasks s = do + let file = oTodofile $ options s + result <- loadLTasks file + case result of + -- TODO: Improve error handling when loading files + Left errorMessage -> pure s{errorPopup=Just $ popupOk "Error loading tasks" errorMessage} + Right ltasks -> pure s{tasks=taskList RTaskList ltasks} + {- Updating the state -} rootBehavior :: UIState -> VTY.Event -> NewState diff --git a/src/TaskMachine/UI/NewTask.hs b/src/TaskMachine/UI/NewTask.hs deleted file mode 100644 index ca119ff..0000000 --- a/src/TaskMachine/UI/NewTask.hs +++ /dev/null @@ -1,8 +0,0 @@ -module TaskMachine.UI.NewTask where - -import qualified Brick as B - -import TaskMachine.UI.Types - -placeholderNewTask :: B.Widget RName -placeholderNewTask = B.str "New: " B.<+> B.vLimit 1 (B.fill '_') diff --git a/src/TaskMachine/UI/TopBar.hs b/src/TaskMachine/UI/TopBar.hs deleted file mode 100644 index 16c708f..0000000 --- a/src/TaskMachine/UI/TopBar.hs +++ /dev/null @@ -1,6 +0,0 @@ -module TaskMachine.UI.TopBar where - -import qualified Brick as B - -placeholderTopBar :: B.Widget n -placeholderTopBar = B.str "Prune | Reload | Search: " B.<+> B.vLimit 1 (B.fill '_') diff --git a/todo.txt b/todo.txt index f37e717..ac537ee 100644 --- a/todo.txt +++ b/todo.txt @@ -3,5 +3,5 @@ - c2018-09-18 Offer "retry" or "quit" +dialogue - syntax error in todo file - c2018-09-18 Offer to clean up file when loading (adding creation/completion dates) - c2018-09-18 Purge - move completed tasks to a separate file -- c2018-09-18 Quit using Esc or q - c2018-09-18 Sort tasks by completion, priority, due date, description +x2018-09-27 c2018-09-18 Quit using Esc or q From 70c11f656d17fc74f1a2b1cd4cb5f54da8ea6a5f Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 28 Sep 2018 17:10:41 +0000 Subject: [PATCH 02/28] Use task list and edit tasks (again) --- package.yaml | 2 +- src/TaskMachine/UI.hs | 57 +++++++++++++++++++++++++++++++--- src/TaskMachine/UI/TaskList.hs | 19 +++++++++--- src/TaskMachine/UI/Types.hs | 1 + todo.txt | 3 ++ 5 files changed, 71 insertions(+), 11 deletions(-) diff --git a/package.yaml b/package.yaml index 41e392b..fa9cbe4 100644 --- a/package.yaml +++ b/package.yaml @@ -32,7 +32,7 @@ dependencies: #- sqlite-simple #- stm #- text - #- text-zipper +- text-zipper - time #- transformers #- unix diff --git a/src/TaskMachine/UI.hs b/src/TaskMachine/UI.hs index 9fddcc5..0cb729e 100644 --- a/src/TaskMachine/UI.hs +++ b/src/TaskMachine/UI.hs @@ -7,11 +7,15 @@ module TaskMachine.UI import qualified Brick as B import qualified Brick.Focus as B import qualified Brick.Themes as B +import qualified Brick.Widgets.Edit as B +import qualified Data.Text.Zipper as T import qualified Data.Vector as V import qualified Graphics.Vty.Input.Events as VTY +import Text.Megaparsec import TaskMachine.LTask import TaskMachine.Options +import TaskMachine.Task import TaskMachine.UI.Popup import TaskMachine.UI.TaskList import TaskMachine.UI.Types @@ -24,8 +28,11 @@ placeholderTopBar = B.str "Prune | Reload | Search: " B.<+> B.vLimit 1 (B.fill ' placeholderNewTask :: B.Widget RName placeholderNewTask = B.str "New: " B.<+> B.vLimit 1 (B.fill '_') +drawTaskList :: Bool -> UIState -> B.Widget RName +drawTaskList focused s = renderTaskList (taskEdit s) focused (tasks s) + drawBaseLayer :: UIState -> B.Widget RName -drawBaseLayer s = B.vBox [placeholderTopBar, renderTaskList True (tasks s), placeholderNewTask] +drawBaseLayer s = B.vBox [placeholderTopBar, drawTaskList True s, placeholderNewTask] drawUIState :: UIState -> [B.Widget RName] drawUIState s@UIState{errorPopup=Just p} = [renderPopupOk p, drawBaseLayer s] @@ -52,16 +59,55 @@ closeBehavior _ s (VTY.EvKey VTY.KEsc []) = B.halt s closeBehavior _ s (VTY.EvKey (VTY.KChar 'q') []) = B.halt s closeBehavior f s e = f s e -- wrapper around another behavior -{- -focusBehavior :: (UIState -> VTY.Event -> Result) -> UIState -> VTY.Event -> Result +focusBehavior :: (UIState -> VTY.Event -> NewState) -> UIState -> VTY.Event -> NewState focusBehavior _ s (VTY.EvKey (VTY.KChar '\t') []) = B.continue $ bigFocusNext s focusBehavior _ s (VTY.EvKey VTY.KBackTab []) = B.continue $ bigFocusPrev s focusBehavior f s e = f s e -- wrapper around another behavior --} + +taskListBehavior :: UIState -> VTY.Event -> NewState +-- Mark/unmark a task as completed +taskListBehavior s (VTY.EvKey (VTY.KChar 'x') []) = undefined s +-- Delete tasks +taskListBehavior s (VTY.EvKey (VTY.KChar 'd') []) = undefined s +-- Start editing a new task +taskListBehavior s (VTY.EvKey (VTY.KChar 'e') []) = + case taskListSelectedElement (tasks s) of + Nothing -> B.continue s -- TODO: Add notification popup + Just t -> + let edit = B.editor RTaskEdit (Just 1) (formatTask t) + in B.continue s{taskEdit=Just edit} +-- Update the task list (scroll etc.) +taskListBehavior s e = do + newTasks <- updateTaskList e (tasks s) + B.continue s{tasks=newTasks} + +taskEditBehavior :: B.Editor String RName -> UIState -> VTY.Event -> NewState +taskEditBehavior _ s (VTY.EvKey VTY.KEsc []) = B.continue s{taskEdit=Nothing} +taskEditBehavior edit s (VTY.EvKey VTY.KHome []) = B.continue s{taskEdit=Just (B.applyEdit T.gotoBOL edit)} +taskEditBehavior edit s (VTY.EvKey VTY.KEnd []) = B.continue s{taskEdit=Just (B.applyEdit T.gotoEOL edit)} +taskEditBehavior edit s (VTY.EvKey VTY.KEnter []) = do -- TODO: Save changes to file + let editedText = unlines $ B.getEditContents edit + case parse pTask "edited task" editedText of + Left parseError -> undefined parseError -- TODO: Add notification here + Right newTask -> do + let newTaskList = taskListModify (const newTask) (tasks s) + B.continue s{tasks=newTaskList, taskEdit=Nothing} +taskEditBehavior edit s e = do + newEdit <- B.handleEditorEvent e edit + B.continue s{taskEdit=Just newEdit} selectBehavior :: UIState -> VTY.Event -> NewState +-- Deal with popup if there is one selectBehavior s@UIState{errorPopup=Just popup} e = undefined popup s e -selectBehavior s e = closeBehavior rootBehavior s e +-- Under the assumption that tasks can only be edited while the task list is focused, edit a task +selectBehavior s@UIState{taskEdit=Just edit} e = taskEditBehavior edit s e +-- If nothing immediately jumps out at you, see which part has focus. +selectBehavior s e = + case B.focusGetCurrent (focus s) of + Just BRTopBar -> closeBehavior (focusBehavior rootBehavior) s e + Just BRTaskList -> closeBehavior (focusBehavior taskListBehavior) s e + Just BRNewTask -> closeBehavior (focusBehavior rootBehavior) s e + Nothing -> closeBehavior (focusBehavior rootBehavior) s e updateUIState :: UIState -> B.BrickEvent RName () -> NewState updateUIState s (B.VtyEvent e) = selectBehavior s e @@ -107,4 +153,5 @@ startUIState o = UIState , focus = B.focusRing [BRTaskList, BRNewTask, BRTopBar] , errorPopup = Nothing , tasks = taskList RTaskList V.empty + , taskEdit = Nothing } diff --git a/src/TaskMachine/UI/TaskList.hs b/src/TaskMachine/UI/TaskList.hs index ec74a2a..d1bb3aa 100644 --- a/src/TaskMachine/UI/TaskList.hs +++ b/src/TaskMachine/UI/TaskList.hs @@ -2,6 +2,7 @@ module TaskMachine.UI.TaskList ( TaskList , taskList , renderTaskList + , updateTaskList , taskListElements , taskListFilter , taskListSelectedElement @@ -11,12 +12,12 @@ module TaskMachine.UI.TaskList --import Data.Void import qualified Brick as B +import qualified Brick.Widgets.Edit as B import qualified Brick.Widgets.List as B import qualified Data.Vector as V +import qualified Graphics.Vty as VTY --import qualified Brick.Focus as B ---import qualified Brick.Widgets.Edit as B --import qualified Data.Text.Zipper as T ---import qualified Graphics.Vty as VTY --import Text.Megaparsec import TaskMachine.LTask @@ -37,9 +38,17 @@ newList name ltasks = B.list name ltasks 1 taskList :: n -> V.Vector LTask -> TaskList n taskList name ltasks = TaskList{visibleTasks=newList name ltasks, invisibleTasks=V.empty} --- TODO: render while editing -renderTaskList :: (Ord n, Show n) => Bool -> TaskList n -> B.Widget n -renderTaskList focus tl = B.renderList (const $ renderTask . toTask) focus (visibleTasks tl) +renderLTask :: (Ord n, Show n) => Maybe (B.Editor String n) -> Bool -> LTask -> B.Widget n +renderLTask (Just e) True _ = B.renderEditor (B.str . unlines) True e +renderLTask _ _ lt = renderTask (toTask lt) + +renderTaskList :: (Ord n, Show n) => Maybe (B.Editor String n) -> Bool -> TaskList n -> B.Widget n +renderTaskList edit focus tl = B.renderList (renderLTask edit) focus (visibleTasks tl) + +updateTaskList :: (Ord n) => VTY.Event -> TaskList n -> B.EventM n (TaskList n) +updateTaskList e tl = do + updatedList <- B.handleListEventVi B.handleListEvent e (visibleTasks tl) + pure tl{visibleTasks=updatedList} {- Managing tasks -} diff --git a/src/TaskMachine/UI/Types.hs b/src/TaskMachine/UI/Types.hs index a73063b..10aa2e5 100644 --- a/src/TaskMachine/UI/Types.hs +++ b/src/TaskMachine/UI/Types.hs @@ -87,6 +87,7 @@ data UIState = UIState -- tasks , tasks :: TaskList RName + , taskEdit :: Maybe (B.Editor String RName) } type NewState = B.EventM RName (B.Next UIState) diff --git a/todo.txt b/todo.txt index ac537ee..a8b7474 100644 --- a/todo.txt +++ b/todo.txt @@ -4,4 +4,7 @@ - c2018-09-18 Offer to clean up file when loading (adding creation/completion dates) - c2018-09-18 Purge - move completed tasks to a separate file - c2018-09-18 Sort tasks by completion, priority, due date, description +- c2018-09-28 Load file in initial app action +- c2018-09-28 Move cursor to beginning of task description when editing tasks +- c2018-09-28 Syntax highlighting while editing tasks x2018-09-27 c2018-09-18 Quit using Esc or q From 11ef9308357b1457e87f25e522b6cd1a8d943db4 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 28 Sep 2018 17:48:08 +0000 Subject: [PATCH 03/28] Move editing related controls to new behavior --- src/TaskMachine/UI.hs | 37 ++++---------------- src/TaskMachine/UI/Behaviors/TaskEdit.hs | 43 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 30 deletions(-) create mode 100644 src/TaskMachine/UI/Behaviors/TaskEdit.hs diff --git a/src/TaskMachine/UI.hs b/src/TaskMachine/UI.hs index 0cb729e..e205469 100644 --- a/src/TaskMachine/UI.hs +++ b/src/TaskMachine/UI.hs @@ -4,18 +4,15 @@ module TaskMachine.UI , loadTasks ) where -import qualified Brick as B -import qualified Brick.Focus as B -import qualified Brick.Themes as B -import qualified Brick.Widgets.Edit as B -import qualified Data.Text.Zipper as T -import qualified Data.Vector as V -import qualified Graphics.Vty.Input.Events as VTY -import Text.Megaparsec +import qualified Brick as B +import qualified Brick.Focus as B +import qualified Brick.Themes as B +import qualified Data.Vector as V +import qualified Graphics.Vty.Input.Events as VTY import TaskMachine.LTask import TaskMachine.Options -import TaskMachine.Task +import TaskMachine.UI.Behaviors.TaskEdit import TaskMachine.UI.Popup import TaskMachine.UI.TaskList import TaskMachine.UI.Types @@ -70,32 +67,12 @@ taskListBehavior s (VTY.EvKey (VTY.KChar 'x') []) = undefined s -- Delete tasks taskListBehavior s (VTY.EvKey (VTY.KChar 'd') []) = undefined s -- Start editing a new task -taskListBehavior s (VTY.EvKey (VTY.KChar 'e') []) = - case taskListSelectedElement (tasks s) of - Nothing -> B.continue s -- TODO: Add notification popup - Just t -> - let edit = B.editor RTaskEdit (Just 1) (formatTask t) - in B.continue s{taskEdit=Just edit} +taskListBehavior s (VTY.EvKey (VTY.KChar 'e') []) = B.continue (startEdit s) -- Update the task list (scroll etc.) taskListBehavior s e = do newTasks <- updateTaskList e (tasks s) B.continue s{tasks=newTasks} -taskEditBehavior :: B.Editor String RName -> UIState -> VTY.Event -> NewState -taskEditBehavior _ s (VTY.EvKey VTY.KEsc []) = B.continue s{taskEdit=Nothing} -taskEditBehavior edit s (VTY.EvKey VTY.KHome []) = B.continue s{taskEdit=Just (B.applyEdit T.gotoBOL edit)} -taskEditBehavior edit s (VTY.EvKey VTY.KEnd []) = B.continue s{taskEdit=Just (B.applyEdit T.gotoEOL edit)} -taskEditBehavior edit s (VTY.EvKey VTY.KEnter []) = do -- TODO: Save changes to file - let editedText = unlines $ B.getEditContents edit - case parse pTask "edited task" editedText of - Left parseError -> undefined parseError -- TODO: Add notification here - Right newTask -> do - let newTaskList = taskListModify (const newTask) (tasks s) - B.continue s{tasks=newTaskList, taskEdit=Nothing} -taskEditBehavior edit s e = do - newEdit <- B.handleEditorEvent e edit - B.continue s{taskEdit=Just newEdit} - selectBehavior :: UIState -> VTY.Event -> NewState -- Deal with popup if there is one selectBehavior s@UIState{errorPopup=Just popup} e = undefined popup s e diff --git a/src/TaskMachine/UI/Behaviors/TaskEdit.hs b/src/TaskMachine/UI/Behaviors/TaskEdit.hs new file mode 100644 index 0000000..b529c63 --- /dev/null +++ b/src/TaskMachine/UI/Behaviors/TaskEdit.hs @@ -0,0 +1,43 @@ +module TaskMachine.UI.Behaviors.TaskEdit + ( startEdit + , taskEditBehavior + ) where + +import qualified Brick as B +import qualified Brick.Widgets.Edit as B +import qualified Data.Text.Zipper as T +import qualified Graphics.Vty as VTY +import Text.Megaparsec + +import TaskMachine.Task +import TaskMachine.UI.TaskList +import TaskMachine.UI.Types + +startEdit :: UIState -> UIState +startEdit s = + case taskListSelectedElement (tasks s) of + Nothing -> undefined -- TODO: Add popup notification + Just t -> + let edit = B.editor RTaskEdit (Just 1) (formatTask t) + in s{taskEdit=Just edit} + +finishEdit :: B.Editor String RName -> UIState -> UIState +finishEdit edit s = + let editedText = unlines $ B.getEditContents edit + in case parse pTask "edited task" editedText of + Left parseError -> undefined parseError -- TODO: Add popup notification + Right newTask -> + let newTaskList = taskListModify (const newTask) (tasks s) + in s{tasks=newTaskList, taskEdit=Nothing} + +taskEditBehavior :: B.Editor String RName -> UIState -> VTY.Event -> NewState +taskEditBehavior _ s (VTY.EvKey VTY.KEsc []) = B.continue s{taskEdit=Nothing} +taskEditBehavior edit s (VTY.EvKey VTY.KHome []) = B.continue s{taskEdit=Just (B.applyEdit T.gotoBOL edit)} +taskEditBehavior edit s (VTY.EvKey VTY.KEnd []) = B.continue s{taskEdit=Just (B.applyEdit T.gotoEOL edit)} +taskEditBehavior edit s (VTY.EvKey VTY.KEnter []) = do + let newState = finishEdit edit s + -- TODO: Save changes to file + B.continue newState +taskEditBehavior edit s e = do + newEdit <- B.handleEditorEvent e edit + B.continue s{taskEdit=Just newEdit} From 7fb6ff4add93a3874e687cdd060f68c3fa30d8f0 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 28 Sep 2018 18:10:51 +0000 Subject: [PATCH 04/28] Move task list related controls to new behavior --- src/TaskMachine/UI.hs | 13 +--------- src/TaskMachine/UI/Behaviors/TaskEdit.hs | 17 +++---------- src/TaskMachine/UI/Behaviors/TaskList.hs | 31 ++++++++++++++++++++++++ src/TaskMachine/UI/Types.hs | 9 ++++++- todo.txt | 1 - 5 files changed, 44 insertions(+), 27 deletions(-) create mode 100644 src/TaskMachine/UI/Behaviors/TaskList.hs diff --git a/src/TaskMachine/UI.hs b/src/TaskMachine/UI.hs index e205469..60148a9 100644 --- a/src/TaskMachine/UI.hs +++ b/src/TaskMachine/UI.hs @@ -13,6 +13,7 @@ import qualified Graphics.Vty.Input.Events as VTY import TaskMachine.LTask import TaskMachine.Options import TaskMachine.UI.Behaviors.TaskEdit +import TaskMachine.UI.Behaviors.TaskList import TaskMachine.UI.Popup import TaskMachine.UI.TaskList import TaskMachine.UI.Types @@ -61,18 +62,6 @@ focusBehavior _ s (VTY.EvKey (VTY.KChar '\t') []) = B.continue $ bigFocusNext s focusBehavior _ s (VTY.EvKey VTY.KBackTab []) = B.continue $ bigFocusPrev s focusBehavior f s e = f s e -- wrapper around another behavior -taskListBehavior :: UIState -> VTY.Event -> NewState --- Mark/unmark a task as completed -taskListBehavior s (VTY.EvKey (VTY.KChar 'x') []) = undefined s --- Delete tasks -taskListBehavior s (VTY.EvKey (VTY.KChar 'd') []) = undefined s --- Start editing a new task -taskListBehavior s (VTY.EvKey (VTY.KChar 'e') []) = B.continue (startEdit s) --- Update the task list (scroll etc.) -taskListBehavior s e = do - newTasks <- updateTaskList e (tasks s) - B.continue s{tasks=newTasks} - selectBehavior :: UIState -> VTY.Event -> NewState -- Deal with popup if there is one selectBehavior s@UIState{errorPopup=Just popup} e = undefined popup s e diff --git a/src/TaskMachine/UI/Behaviors/TaskEdit.hs b/src/TaskMachine/UI/Behaviors/TaskEdit.hs index b529c63..ae0cf08 100644 --- a/src/TaskMachine/UI/Behaviors/TaskEdit.hs +++ b/src/TaskMachine/UI/Behaviors/TaskEdit.hs @@ -1,6 +1,5 @@ module TaskMachine.UI.Behaviors.TaskEdit - ( startEdit - , taskEditBehavior + ( taskEditBehavior ) where import qualified Brick as B @@ -13,14 +12,6 @@ import TaskMachine.Task import TaskMachine.UI.TaskList import TaskMachine.UI.Types -startEdit :: UIState -> UIState -startEdit s = - case taskListSelectedElement (tasks s) of - Nothing -> undefined -- TODO: Add popup notification - Just t -> - let edit = B.editor RTaskEdit (Just 1) (formatTask t) - in s{taskEdit=Just edit} - finishEdit :: B.Editor String RName -> UIState -> UIState finishEdit edit s = let editedText = unlines $ B.getEditContents edit @@ -34,10 +25,10 @@ taskEditBehavior :: B.Editor String RName -> UIState -> VTY.Event -> NewState taskEditBehavior _ s (VTY.EvKey VTY.KEsc []) = B.continue s{taskEdit=Nothing} taskEditBehavior edit s (VTY.EvKey VTY.KHome []) = B.continue s{taskEdit=Just (B.applyEdit T.gotoBOL edit)} taskEditBehavior edit s (VTY.EvKey VTY.KEnd []) = B.continue s{taskEdit=Just (B.applyEdit T.gotoEOL edit)} -taskEditBehavior edit s (VTY.EvKey VTY.KEnter []) = do +taskEditBehavior edit s (VTY.EvKey VTY.KEnter []) = B.suspendAndResume $ do let newState = finishEdit edit s - -- TODO: Save changes to file - B.continue newState + saveUIState newState + pure newState taskEditBehavior edit s e = do newEdit <- B.handleEditorEvent e edit B.continue s{taskEdit=Just newEdit} diff --git a/src/TaskMachine/UI/Behaviors/TaskList.hs b/src/TaskMachine/UI/Behaviors/TaskList.hs new file mode 100644 index 0000000..cfe5fff --- /dev/null +++ b/src/TaskMachine/UI/Behaviors/TaskList.hs @@ -0,0 +1,31 @@ +module TaskMachine.UI.Behaviors.TaskList + ( taskListBehavior + ) where + +import qualified Brick as B +import qualified Brick.Widgets.Edit as B +import qualified Graphics.Vty as VTY + +import TaskMachine.Task +import TaskMachine.UI.TaskList +import TaskMachine.UI.Types + +startEdit :: UIState -> UIState +startEdit s = + case taskListSelectedElement (tasks s) of + Nothing -> undefined -- TODO: Add popup notification + Just t -> + let edit = B.editor RTaskEdit (Just 1) (formatTask t) + in s{taskEdit=Just edit} + +taskListBehavior :: UIState -> VTY.Event -> NewState +-- Mark/unmark a task as completed +taskListBehavior s (VTY.EvKey (VTY.KChar 'x') []) = undefined s +-- Delete tasks +taskListBehavior s (VTY.EvKey (VTY.KChar 'd') []) = undefined s +-- Start editing a new task +taskListBehavior s (VTY.EvKey (VTY.KChar 'e') []) = B.continue (startEdit s) +-- Update the task list (scroll etc.) +taskListBehavior s e = do + newTasks <- updateTaskList e (tasks s) + B.continue s{tasks=newTasks} diff --git a/src/TaskMachine/UI/Types.hs b/src/TaskMachine/UI/Types.hs index 10aa2e5..157d7de 100644 --- a/src/TaskMachine/UI/Types.hs +++ b/src/TaskMachine/UI/Types.hs @@ -19,6 +19,7 @@ module TaskMachine.UI.Types , bigFocusNext, bigFocusPrev --, smallFocusNext, smallFocusPrev , defaultTheme + , saveUIState ) where import qualified Brick as B @@ -30,7 +31,7 @@ import qualified Brick.Widgets.List as B import qualified Graphics.Vty as VTY --import qualified Data.Vector as V ---import TaskMachine.LTask +import TaskMachine.LTask import TaskMachine.Options import TaskMachine.UI.Popup import TaskMachine.UI.Task @@ -180,3 +181,9 @@ defaultTheme = B.newTheme VTY.defAttr bg' = VTY.withBackColor none st' = VTY.withStyle none none = VTY.defAttr + +saveUIState :: UIState -> IO () +saveUIState s = do + let filepath = oTodofile (options s) + ltasks = taskListElements (tasks s) + saveLTasks filepath ltasks diff --git a/todo.txt b/todo.txt index a8b7474..668e9be 100644 --- a/todo.txt +++ b/todo.txt @@ -4,7 +4,6 @@ - c2018-09-18 Offer to clean up file when loading (adding creation/completion dates) - c2018-09-18 Purge - move completed tasks to a separate file - c2018-09-18 Sort tasks by completion, priority, due date, description -- c2018-09-28 Load file in initial app action - c2018-09-28 Move cursor to beginning of task description when editing tasks - c2018-09-28 Syntax highlighting while editing tasks x2018-09-27 c2018-09-18 Quit using Esc or q From f72553987504e1094eb973c5a17313ab0aa18b4c Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 28 Sep 2018 21:41:04 +0000 Subject: [PATCH 05/28] Rename executable to task-machine --- package.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.yaml b/package.yaml index fa9cbe4..c338c30 100644 --- a/package.yaml +++ b/package.yaml @@ -47,7 +47,7 @@ library: source-dirs: src executables: - task-machine-exe: + task-machine: main: Main.hs source-dirs: app ghc-options: From a8bdc47b2c0722e0e72166f87bc2257e60dd7ef7 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 28 Sep 2018 21:42:46 +0000 Subject: [PATCH 06/28] Switch to MIT license --- LICENSE | 43 +++++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/LICENSE b/LICENSE index 5714ffc..26bc945 100644 --- a/LICENSE +++ b/LICENSE @@ -1,30 +1,21 @@ -Copyright Joscha Mennicken (c) 2018 +MIT License -All rights reserved. +Copyright (c) 2018 Joscha Mennicken -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * Neither the name of Joscha Mennicken nor the names of other - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From efeeef39eb9b93c794f2c98aaf409f14bb95c4fe Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 29 Sep 2018 10:59:36 +0000 Subject: [PATCH 07/28] Use liftIO instead of suspendAndResume --- package.yaml | 1 + src/TaskMachine/UI/Behaviors/TaskEdit.hs | 7 ++++--- todo.txt | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/package.yaml b/package.yaml index c338c30..595fb48 100644 --- a/package.yaml +++ b/package.yaml @@ -28,6 +28,7 @@ dependencies: #- bytestring - containers - megaparsec +- mtl - optparse-applicative #- sqlite-simple #- stm diff --git a/src/TaskMachine/UI/Behaviors/TaskEdit.hs b/src/TaskMachine/UI/Behaviors/TaskEdit.hs index ae0cf08..b251cf4 100644 --- a/src/TaskMachine/UI/Behaviors/TaskEdit.hs +++ b/src/TaskMachine/UI/Behaviors/TaskEdit.hs @@ -4,6 +4,7 @@ module TaskMachine.UI.Behaviors.TaskEdit import qualified Brick as B import qualified Brick.Widgets.Edit as B +import Control.Monad.Trans import qualified Data.Text.Zipper as T import qualified Graphics.Vty as VTY import Text.Megaparsec @@ -25,10 +26,10 @@ taskEditBehavior :: B.Editor String RName -> UIState -> VTY.Event -> NewState taskEditBehavior _ s (VTY.EvKey VTY.KEsc []) = B.continue s{taskEdit=Nothing} taskEditBehavior edit s (VTY.EvKey VTY.KHome []) = B.continue s{taskEdit=Just (B.applyEdit T.gotoBOL edit)} taskEditBehavior edit s (VTY.EvKey VTY.KEnd []) = B.continue s{taskEdit=Just (B.applyEdit T.gotoEOL edit)} -taskEditBehavior edit s (VTY.EvKey VTY.KEnter []) = B.suspendAndResume $ do +taskEditBehavior edit s (VTY.EvKey VTY.KEnter []) = do let newState = finishEdit edit s - saveUIState newState - pure newState + liftIO $ saveUIState newState + B.continue newState taskEditBehavior edit s e = do newEdit <- B.handleEditorEvent e edit B.continue s{taskEdit=Just newEdit} diff --git a/todo.txt b/todo.txt index 668e9be..4cb4270 100644 --- a/todo.txt +++ b/todo.txt @@ -7,3 +7,4 @@ - c2018-09-28 Move cursor to beginning of task description when editing tasks - c2018-09-28 Syntax highlighting while editing tasks x2018-09-27 c2018-09-18 Quit using Esc or q +x2018-09-29 c2018-09-28 Use B.EventM's MonadIO instance instead of B.suspendAndResume (facepalm) From 55e12992b348499ad554f4cc45cafd24ddfaafa5 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 29 Sep 2018 11:11:58 +0000 Subject: [PATCH 08/28] Clean up old stuff --- src/TaskMachine/UI.hs | 74 +--------- src/TaskMachine/UI/Behaviors/TaskEdit.hs | 2 +- src/TaskMachine/UI/TaskList.hs | 170 ----------------------- src/TaskMachine/UI/Types.hs | 136 +++--------------- 4 files changed, 27 insertions(+), 355 deletions(-) diff --git a/src/TaskMachine/UI.hs b/src/TaskMachine/UI.hs index 60148a9..1c1d375 100644 --- a/src/TaskMachine/UI.hs +++ b/src/TaskMachine/UI.hs @@ -5,103 +5,44 @@ module TaskMachine.UI ) where import qualified Brick as B -import qualified Brick.Focus as B import qualified Brick.Themes as B import qualified Data.Vector as V import qualified Graphics.Vty.Input.Events as VTY -import TaskMachine.LTask import TaskMachine.Options import TaskMachine.UI.Behaviors.TaskEdit import TaskMachine.UI.Behaviors.TaskList -import TaskMachine.UI.Popup import TaskMachine.UI.TaskList import TaskMachine.UI.Types {- Rendering -} -placeholderTopBar :: B.Widget n -placeholderTopBar = B.str "Prune | Reload | Search: " B.<+> B.vLimit 1 (B.fill '_') - -placeholderNewTask :: B.Widget RName -placeholderNewTask = B.str "New: " B.<+> B.vLimit 1 (B.fill '_') - -drawTaskList :: Bool -> UIState -> B.Widget RName -drawTaskList focused s = renderTaskList (taskEdit s) focused (tasks s) - -drawBaseLayer :: UIState -> B.Widget RName -drawBaseLayer s = B.vBox [placeholderTopBar, drawTaskList True s, placeholderNewTask] +drawTaskList :: UIState -> B.Widget RName +drawTaskList s = renderTaskList (taskEdit s) True (tasks s) drawUIState :: UIState -> [B.Widget RName] -drawUIState s@UIState{errorPopup=Just p} = [renderPopupOk p, drawBaseLayer s] -drawUIState s = [drawBaseLayer s] - -{- Actions -} - -loadTasks :: UIState -> IO UIState -loadTasks s = do - let file = oTodofile $ options s - result <- loadLTasks file - case result of - -- TODO: Improve error handling when loading files - Left errorMessage -> pure s{errorPopup=Just $ popupOk "Error loading tasks" errorMessage} - Right ltasks -> pure s{tasks=taskList RTaskList ltasks} +--drawUIState s@UIState{errorPopup=Just p} = [renderPopupOk p, drawTaskList s] +drawUIState s = [drawTaskList s] {- Updating the state -} -rootBehavior :: UIState -> VTY.Event -> NewState -rootBehavior s _ = B.continue s - closeBehavior :: (UIState -> VTY.Event -> NewState) -> UIState -> VTY.Event -> NewState closeBehavior _ s (VTY.EvKey VTY.KEsc []) = B.halt s closeBehavior _ s (VTY.EvKey (VTY.KChar 'q') []) = B.halt s closeBehavior f s e = f s e -- wrapper around another behavior -focusBehavior :: (UIState -> VTY.Event -> NewState) -> UIState -> VTY.Event -> NewState -focusBehavior _ s (VTY.EvKey (VTY.KChar '\t') []) = B.continue $ bigFocusNext s -focusBehavior _ s (VTY.EvKey VTY.KBackTab []) = B.continue $ bigFocusPrev s -focusBehavior f s e = f s e -- wrapper around another behavior - selectBehavior :: UIState -> VTY.Event -> NewState -- Deal with popup if there is one -selectBehavior s@UIState{errorPopup=Just popup} e = undefined popup s e +--selectBehavior s@UIState{errorPopup=Just popup} e = undefined popup s e -- Under the assumption that tasks can only be edited while the task list is focused, edit a task selectBehavior s@UIState{taskEdit=Just edit} e = taskEditBehavior edit s e -- If nothing immediately jumps out at you, see which part has focus. -selectBehavior s e = - case B.focusGetCurrent (focus s) of - Just BRTopBar -> closeBehavior (focusBehavior rootBehavior) s e - Just BRTaskList -> closeBehavior (focusBehavior taskListBehavior) s e - Just BRNewTask -> closeBehavior (focusBehavior rootBehavior) s e - Nothing -> closeBehavior (focusBehavior rootBehavior) s e +selectBehavior s e = closeBehavior taskListBehavior s e updateUIState :: UIState -> B.BrickEvent RName () -> NewState updateUIState s (B.VtyEvent e) = selectBehavior s e updateUIState s _ = B.continue s -{- -updateUIState :: UIState -> B.BrickEvent RName () -> B.EventM RName (B.Next UIState) --- Closing error popup -updateUIState s@UIState{errorPopup=Just _} (B.VtyEvent (VTY.EvKey VTY.KEnter [])) = B.continue s{errorPopup=Nothing} -updateUIState s@UIState{errorPopup=Just _} (B.VtyEvent (VTY.EvKey VTY.KEsc [])) = B.continue s{errorPopup=Nothing} ---updateUIState s@UIState{errorPopup=Just p} (B.VtyEvent e) = do --- newPopup <- handlePopupEvent e p --- B.continue s{errorPopup=Just newPopup} -updateUIState s e = - case B.focusGetCurrent (focus s) of - Nothing -> B.halt s - (Just BRTopBar) -> placeholderUpdate s e - --(Just BRTaskList) -> updateTaskList s e - (Just BRTaskList) -> placeholderUpdate s e - (Just BRNewTask) -> placeholderUpdate s e - -placeholderUpdate :: UIState -> B.BrickEvent RName () -> B.EventM RName (B.Next UIState) -placeholderUpdate s (B.VtyEvent (VTY.EvKey VTY.KEsc [])) = B.halt s -placeholderUpdate s (B.VtyEvent (VTY.EvKey (VTY.KChar '\t') [])) = B.continue $ bigFocusNext s -placeholderUpdate s (B.VtyEvent (VTY.EvKey VTY.KBackTab [])) = B.continue $ bigFocusPrev s -placeholderUpdate s _ = B.continue s --} - {- Starting the app -} myApp :: B.App UIState () RName @@ -116,8 +57,7 @@ myApp = B.App startUIState :: Options -> UIState startUIState o = UIState { options = o - , focus = B.focusRing [BRTaskList, BRNewTask, BRTopBar] - , errorPopup = Nothing + --, errorPopup = Nothing , tasks = taskList RTaskList V.empty , taskEdit = Nothing } diff --git a/src/TaskMachine/UI/Behaviors/TaskEdit.hs b/src/TaskMachine/UI/Behaviors/TaskEdit.hs index b251cf4..dc495fb 100644 --- a/src/TaskMachine/UI/Behaviors/TaskEdit.hs +++ b/src/TaskMachine/UI/Behaviors/TaskEdit.hs @@ -28,7 +28,7 @@ taskEditBehavior edit s (VTY.EvKey VTY.KHome []) = B.continue s{taskEdit=Just ( taskEditBehavior edit s (VTY.EvKey VTY.KEnd []) = B.continue s{taskEdit=Just (B.applyEdit T.gotoEOL edit)} taskEditBehavior edit s (VTY.EvKey VTY.KEnter []) = do let newState = finishEdit edit s - liftIO $ saveUIState newState + liftIO $ saveTasks newState B.continue newState taskEditBehavior edit s e = do newEdit <- B.handleEditorEvent e edit diff --git a/src/TaskMachine/UI/TaskList.hs b/src/TaskMachine/UI/TaskList.hs index d1bb3aa..4423904 100644 --- a/src/TaskMachine/UI/TaskList.hs +++ b/src/TaskMachine/UI/TaskList.hs @@ -9,23 +9,15 @@ module TaskMachine.UI.TaskList , taskListModify ) where ---import Data.Void - import qualified Brick as B import qualified Brick.Widgets.Edit as B import qualified Brick.Widgets.List as B import qualified Data.Vector as V import qualified Graphics.Vty as VTY ---import qualified Brick.Focus as B ---import qualified Data.Text.Zipper as T ---import Text.Megaparsec import TaskMachine.LTask import TaskMachine.Task import TaskMachine.UI.Task ---import TaskMachine.Options ---import TaskMachine.UI.Popup ---import TaskMachine.UI.Types data TaskList n = TaskList { visibleTasks :: B.List n LTask @@ -69,165 +61,3 @@ taskListModify :: (Task -> Task) -> TaskList n -> TaskList n taskListModify f tl = let list = B.listModify (modifyLTask f) (visibleTasks tl) in tl{visibleTasks=list} - -{- -{- Managing the tasks -} - -allTasks :: UIState -> V.Vector LTask -allTasks s = - let visible = B.listElements $ taskList s - invisible = invisibleTasks s - in visible <> invisible - -newTaskList :: V.Vector LTask -> B.List RName LTask -newTaskList ltasks = B.list RTaskList ltasks 1 - --- TODO: Catch errors when loading tasks -loadTasks :: UIState -> IO UIState -loadTasks s = do - let file = oTodofile $ options s - result <- loadLTasks file - case result of - Left errorMessage -> pure s{errorPopup=Just $ popupOk "Error loading tasks" errorMessage} - Right ltasks -> pure s{taskList=newTaskList ltasks, invisibleTasks=V.empty} - --- TODO: Catch errors when saving tasks -saveTasks :: UIState -> IO UIState -saveTasks s = do - let file = oTodofile $ options s - ltasks = allTasks s - saveLTasks file ltasks - pure s - -filterTasks :: (Task -> Bool) -> UIState -> UIState -filterTasks f s = - let (yes, no) = V.partition (f . toTask) (allTasks s) - in s{taskList=newTaskList yes, invisibleTasks=no} - -{- Rendering -} - -renderLTask :: Maybe (B.Editor String RName) -> Bool -> LTask -> B.Widget RName -renderLTask _ False ltask = renderTask $ toTask ltask -renderLTask Nothing True ltask = renderTask $ toTask ltask -renderLTask _ _ _ = undefined ---renderLTask (Just edit) True _ = B.renderEditor (B.str . unlines) True edit - -renderTaskList :: UIState -> B.Widget RName -renderTaskList s = - let inFocus = B.focusGetCurrent (focus s) == Just BRTaskList - in B.renderList (renderLTask Nothing) inFocus (taskList s) - -{- Updating state -} - -taskListBehavior :: UIState -> VTY.Event -> NewState -taskListBehavior = undefined - -updateTaskList :: UIState -> B.BrickEvent RName () -> B.EventM RName (B.Next UIState) -updateTaskList = undefined --} - -{- -widgetPriority :: B.AttrName -> Maybe Priority -> B.Widget n -widgetPriority _ Nothing = B.emptyWidget -widgetPriority highlight (Just prio) = - let attrName = highlight <> "priority" <> B.attrName [priorityToChar prio] - text = formatPriority prio ++ " " - in B.withAttr attrName $ B.str text - -widgetDescription :: B.AttrName -> String -> B.Widget n -widgetDescription highlight desc = - let attrName = highlight <> "description" - in B.withAttr attrName $ B.str desc - -renderLTask :: Bool -> LTask -> B.Widget RName -renderLTask highlight (LTask _ Task{..}) = - let attrHighlight = if highlight then "highlight" else "normal" - wCompleted = B.str $ if taskCompleted then "x " else " " - wPriority = widgetPriority attrHighlight taskPriority - wDescription = widgetDescription attrHighlight taskDescription - in B.hBox [wCompleted, wPriority, wDescription] --} - ---type Editor = B.Editor String RName ---type TaskList = B.List RName LTask - -{- Editing tasks -} - -{- -toEditText :: Task -> String -toEditText Task{taskPriority=Nothing, taskDescription=d} = descriptionToString d -toEditText Task{taskPriority=Just p, taskDescription=d} = formatPriority p ++ " " ++ descriptionToString d - -pEditText :: Parser (Maybe Priority, String) -pEditText = undefined ---pEditText = do --- prio <- maybeParse (andSpace pPriority) --- notFollowedBy (andSpace pDates) --- desc <- untilEndOfLine --- pure (prio, desc) - -parseEditText :: String -> Either (ParseError Char Void) (Maybe Priority, String) -parseEditText = parse pEditText "edited task" --} - -{- Updating state -} - -{- -startEdit :: UIState -> UIState -startEdit s = - case B.listSelectedElement (taskList s) of - Nothing -> s - Just (_, LTask _ t) -> - let edit = B.editor RTaskEdit (Just 1) (toEditText t) - in s{taskEdit=Just edit} - -finishEdit :: UIState -> UIState -finishEdit s@UIState{taskEdit=Just edit} = - case B.getEditContents edit of - [line] -> case parseEditText line of - Right (prio, desc) -> - --let changeTask (LTask n t) = LTask n t{taskPriority=prio, taskDescription=desc} - let changeTask (LTask n t) = LTask n t{taskPriority=prio, taskDescription=undefined desc} - newList = B.listModify changeTask (taskList s) - in s{taskList=newList, taskEdit=Nothing} - - Left parseError -> s{errorPopup=Just $ popup "Parse error" (parseErrorTextPretty parseError)} - _ -> s{errorPopup=Just $ popup "Empty editor" "Enter a task description."} -finishEdit s = s - -updateEditor :: B.Editor String RName -> VTY.Event -> B.EventM RName (B.Editor String RName) -updateEditor edit (VTY.EvKey VTY.KHome []) = pure $ B.applyEdit T.gotoBOL edit -updateEditor edit (VTY.EvKey VTY.KEnd []) = pure $ B.applyEdit T.gotoEOL edit -updateEditor edit e = B.handleEditorEvent e edit - -updateTaskList :: UIState -> B.BrickEvent RName () -> B.EventM RName (B.Next UIState) --- Exit application -updateTaskList s@UIState{taskEdit=Nothing} (B.VtyEvent (VTY.EvKey VTY.KEsc [])) = B.halt s - --- Test stuff -updateTaskList s e = do - let changeTask (LTask n t) = LTask n t{taskDescription=show e} - newList = B.listModify changeTask (taskList s) - B.continue s{taskList=newList} - --- Scroll focus -updateTaskList s (B.VtyEvent (VTY.EvKey VTY.KBackTab [])) = B.continue $ bigFocusPrev s -updateTaskList s (B.VtyEvent (VTY.EvKey (VTY.KChar '\t') [])) = B.continue $ bigFocusNext s --- Start editing the current task -updateTaskList s@UIState{taskEdit=Nothing} (B.VtyEvent (VTY.EvKey (VTY.KChar 'e') [])) = B.continue $ startEdit s --- Update the task list -updateTaskList s@UIState{taskEdit=Nothing} (B.VtyEvent e) = do - newList <- B.handleListEventVi B.handleListEvent e (taskList s) - B.continue s{taskList=newList} --- Exit the editor (losing all changes) -updateTaskList s@UIState{taskEdit=Just _} (B.VtyEvent (VTY.EvKey VTY.KEsc [])) = B.continue $ s{taskEdit=Nothing} --- Exit the editor (keeping all changes) -updateTaskList s@UIState{taskEdit=Just _} (B.VtyEvent (VTY.EvKey VTY.KEnter [])) = B.continue $ finishEdit s --- Update the editor -updateTaskList s@UIState{taskEdit=Just edit} (B.VtyEvent e) = do - newTaskEdit <- updateEditor edit e - B.continue s{taskEdit=Just newTaskEdit} --- Catch everything else -updateTaskList s _ = B.halt s ---updateTaskList list (Just editor) (B.VtyEvent e) = (,) <$> const list <*> B.handleEditorEvent e editor --} diff --git a/src/TaskMachine/UI/Types.hs b/src/TaskMachine/UI/Types.hs index 157d7de..7500ea7 100644 --- a/src/TaskMachine/UI/Types.hs +++ b/src/TaskMachine/UI/Types.hs @@ -6,151 +6,43 @@ module TaskMachine.UI.Types ( RName(..) - , BigRing(..) - --, SmallRing(..) - -- * Popups - --, Popup - --, popup - --, renderPopup - --, handlePopupEvent -- * UI state , UIState(..) , NewState - , bigFocusNext, bigFocusPrev - --, smallFocusNext, smallFocusPrev , defaultTheme - , saveUIState + , loadTasks + , saveTasks ) where import qualified Brick as B -import qualified Brick.Focus as B import qualified Brick.Themes as B import qualified Brick.Widgets.Dialog as B import qualified Brick.Widgets.Edit as B import qualified Brick.Widgets.List as B import qualified Graphics.Vty as VTY ---import qualified Data.Vector as V import TaskMachine.LTask import TaskMachine.Options -import TaskMachine.UI.Popup import TaskMachine.UI.Task import TaskMachine.UI.TaskList -- | Resource names data RName - = RSearchEdit - | RTaskList + = RTaskList | RTaskEdit - | RNewEdit deriving (Eq, Show, Ord) -data BigRing - = BRTopBar - | BRTaskList - | BRNewTask - deriving (Eq) - -{- -data SmallRing - = SRPurge - | SRReload - | SRSearch - deriving (Eq) --} - -{- Popup -} - -{- -data Popup = Popup (B.Dialog ()) (B.Widget RName) - -popup :: String -> String -> Popup -popup title content = - let dialog = B.dialog (Just title) (Just (0,[("OK",())])) 70 -- with a min terminal width of 80 - widget = B.str content - in Popup dialog widget - -renderPopup :: Popup -> B.Widget RName -renderPopup (Popup dialog widget) = B.renderDialog dialog widget - -handlePopupEvent :: VTY.Event -> Popup -> B.EventM RName Popup -handlePopupEvent e (Popup dialog widget) = Popup <$> B.handleDialogEvent e dialog <*> pure widget --} - {- UI state -} data UIState = UIState - { options :: Options -- includes todo file and other config - , focus :: B.FocusRing BigRing -- focus on the top, middle or bottom part - - -- popups - , errorPopup :: Maybe (PopupOk RName) - - -- tasks - , tasks :: TaskList RName - , taskEdit :: Maybe (B.Editor String RName) + { options :: Options -- includes todo file and other config + --, errorPopup :: Maybe (PopupOk RName) + , tasks :: TaskList RName + , taskEdit :: Maybe (B.Editor String RName) } type NewState = B.EventM RName (B.Next UIState) - - - - - - -{- - , focus :: B.FocusRing BigRing - -- ^ 'B.FocusRing' for tab navigation - --, focusTopBar :: B.FocusRing SmallRing - -- ^ 'B.FocusRing' for the top bar, for ← and → arrow key navigation - , errorPopup :: Maybe Popup - - -- TOP BAR - --, searchEdit :: B.Editor String RName - -- ^ Content of the search field - - -- TASK LIST - , taskList :: B.List RName LTask - -- ^ List to display tasks - , invisibleTasks :: V.Vector LTask - -- ^ All tasks that aren't displayed in the taskList due to search filters - , taskEdit :: Maybe (B.Editor String RName) - -- ^ Task currently being edited - - -- NEW TASK - --, newEdit :: B.Editor String RName - -- ^ "New task" text field at the bottom - } - --- | Create a starting UI state -startUIState :: V.Vector LTask -> UIState -startUIState ltasks = UIState - { focus = B.focusRing [BRTaskList, BRNewTask, BRTopBar] - --, focusTopBar = B.focusRing [SRPrune, SRReload, SRSearch] - , errorPopup = Nothing - --, searchEdit = B.editor RSearchEdit (Just 1) "" - , taskList = B.list RTaskList ltasks 1 - , invisibleTasks = V.empty - , taskEdit = Nothing - --, newEdit = B.editor RNewEdit (Just 1) "" - } --} - -bigFocusNext :: UIState -> UIState -bigFocusNext s = s{focus=B.focusNext (focus s)} - -bigFocusPrev :: UIState -> UIState -bigFocusPrev s = s{focus=B.focusPrev (focus s)} - -{- -smallFocusNext :: UIState -> UIState -smallFocusNext s = s{focusTopBar=B.focusNext (focusTopBar s)} - -smallFocusPrev :: UIState -> UIState -smallFocusPrev s = s{focusTopBar=B.focusPrev (focusTopBar s)} --} - defaultTheme :: B.Theme defaultTheme = B.newTheme VTY.defAttr [ (B.dialogAttr, none) @@ -182,8 +74,18 @@ defaultTheme = B.newTheme VTY.defAttr st' = VTY.withStyle none none = VTY.defAttr -saveUIState :: UIState -> IO () -saveUIState s = do +loadTasks :: UIState -> IO UIState +loadTasks s = do + let file = oTodofile $ options s + result <- loadLTasks file + case result of + -- TODO: Improve error handling when loading files + --Left errorMessage -> pure s{errorPopup=Just $ popupOk "Error loading tasks" errorMessage} + Left errorMessage -> undefined errorMessage + Right ltasks -> pure s{tasks=taskList RTaskList ltasks} + +saveTasks :: UIState -> IO () +saveTasks s = do let filepath = oTodofile (options s) ltasks = taskListElements (tasks s) saveLTasks filepath ltasks From 3f88a247ce10af1d5304589f99ecdbc1e58b880a Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 29 Sep 2018 11:21:51 +0000 Subject: [PATCH 09/28] Clean up behaviors --- src/TaskMachine/UI.hs | 11 +++---- .../{Behaviors/TaskEdit.hs => Behaviors.hs} | 25 +++++++++++++-- src/TaskMachine/UI/Behaviors/TaskList.hs | 31 ------------------- 3 files changed, 28 insertions(+), 39 deletions(-) rename src/TaskMachine/UI/{Behaviors/TaskEdit.hs => Behaviors.hs} (63%) delete mode 100644 src/TaskMachine/UI/Behaviors/TaskList.hs diff --git a/src/TaskMachine/UI.hs b/src/TaskMachine/UI.hs index 1c1d375..12a13bf 100644 --- a/src/TaskMachine/UI.hs +++ b/src/TaskMachine/UI.hs @@ -4,14 +4,13 @@ module TaskMachine.UI , loadTasks ) where -import qualified Brick as B -import qualified Brick.Themes as B -import qualified Data.Vector as V -import qualified Graphics.Vty.Input.Events as VTY +import qualified Brick as B +import qualified Brick.Themes as B +import qualified Data.Vector as V +import qualified Graphics.Vty.Input.Events as VTY import TaskMachine.Options -import TaskMachine.UI.Behaviors.TaskEdit -import TaskMachine.UI.Behaviors.TaskList +import TaskMachine.UI.Behaviors import TaskMachine.UI.TaskList import TaskMachine.UI.Types diff --git a/src/TaskMachine/UI/Behaviors/TaskEdit.hs b/src/TaskMachine/UI/Behaviors.hs similarity index 63% rename from src/TaskMachine/UI/Behaviors/TaskEdit.hs rename to src/TaskMachine/UI/Behaviors.hs index dc495fb..a8358e3 100644 --- a/src/TaskMachine/UI/Behaviors/TaskEdit.hs +++ b/src/TaskMachine/UI/Behaviors.hs @@ -1,5 +1,6 @@ -module TaskMachine.UI.Behaviors.TaskEdit - ( taskEditBehavior +module TaskMachine.UI.Behaviors + ( taskListBehavior + , taskEditBehavior ) where import qualified Brick as B @@ -13,6 +14,14 @@ import TaskMachine.Task import TaskMachine.UI.TaskList import TaskMachine.UI.Types +startEdit :: UIState -> UIState +startEdit s = + case taskListSelectedElement (tasks s) of + Nothing -> undefined -- TODO: Add popup notification + Just t -> + let edit = B.editor RTaskEdit (Just 1) (formatTask t) + in s{taskEdit=Just edit} + finishEdit :: B.Editor String RName -> UIState -> UIState finishEdit edit s = let editedText = unlines $ B.getEditContents edit @@ -33,3 +42,15 @@ taskEditBehavior edit s (VTY.EvKey VTY.KEnter []) = do taskEditBehavior edit s e = do newEdit <- B.handleEditorEvent e edit B.continue s{taskEdit=Just newEdit} + +taskListBehavior :: UIState -> VTY.Event -> NewState +-- Mark/unmark a task as completed +taskListBehavior s (VTY.EvKey (VTY.KChar 'x') []) = undefined s +-- Delete tasks +taskListBehavior s (VTY.EvKey (VTY.KChar 'd') []) = undefined s +-- Start editing a new task +taskListBehavior s (VTY.EvKey (VTY.KChar 'e') []) = B.continue (startEdit s) +-- Update the task list (scroll etc.) +taskListBehavior s e = do + newTasks <- updateTaskList e (tasks s) + B.continue s{tasks=newTasks} diff --git a/src/TaskMachine/UI/Behaviors/TaskList.hs b/src/TaskMachine/UI/Behaviors/TaskList.hs deleted file mode 100644 index cfe5fff..0000000 --- a/src/TaskMachine/UI/Behaviors/TaskList.hs +++ /dev/null @@ -1,31 +0,0 @@ -module TaskMachine.UI.Behaviors.TaskList - ( taskListBehavior - ) where - -import qualified Brick as B -import qualified Brick.Widgets.Edit as B -import qualified Graphics.Vty as VTY - -import TaskMachine.Task -import TaskMachine.UI.TaskList -import TaskMachine.UI.Types - -startEdit :: UIState -> UIState -startEdit s = - case taskListSelectedElement (tasks s) of - Nothing -> undefined -- TODO: Add popup notification - Just t -> - let edit = B.editor RTaskEdit (Just 1) (formatTask t) - in s{taskEdit=Just edit} - -taskListBehavior :: UIState -> VTY.Event -> NewState --- Mark/unmark a task as completed -taskListBehavior s (VTY.EvKey (VTY.KChar 'x') []) = undefined s --- Delete tasks -taskListBehavior s (VTY.EvKey (VTY.KChar 'd') []) = undefined s --- Start editing a new task -taskListBehavior s (VTY.EvKey (VTY.KChar 'e') []) = B.continue (startEdit s) --- Update the task list (scroll etc.) -taskListBehavior s e = do - newTasks <- updateTaskList e (tasks s) - B.continue s{tasks=newTasks} From 9fc57bd05654b87c5947e49d4652e3dcf1070c39 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 29 Sep 2018 11:30:07 +0000 Subject: [PATCH 10/28] Reload tasks when pressing 'r' --- src/TaskMachine/UI/Behaviors.hs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/TaskMachine/UI/Behaviors.hs b/src/TaskMachine/UI/Behaviors.hs index a8358e3..5a76d7a 100644 --- a/src/TaskMachine/UI/Behaviors.hs +++ b/src/TaskMachine/UI/Behaviors.hs @@ -44,6 +44,10 @@ taskEditBehavior edit s e = do B.continue s{taskEdit=Just newEdit} taskListBehavior :: UIState -> VTY.Event -> NewState +-- Reload while running +taskListBehavior s (VTY.EvKey (VTY.KChar 'r') []) = do + newState <- liftIO $ loadTasks s + B.continue newState -- Mark/unmark a task as completed taskListBehavior s (VTY.EvKey (VTY.KChar 'x') []) = undefined s -- Delete tasks From cad2f5741f40acbfd665d9bd626f7162ed8a00a0 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 30 Sep 2018 17:35:57 +0000 Subject: [PATCH 11/28] Load and save tasks This commit includes new popups and proper error messages. --- app/Main.hs | 3 +- src/TaskMachine/LTask.hs | 47 +++++++++++++++++++++++----- src/TaskMachine/UI.hs | 31 ++++++++++++++----- src/TaskMachine/UI/Behaviors.hs | 8 ++--- src/TaskMachine/UI/Popup.hs | 37 ++++++++++++---------- src/TaskMachine/UI/Stuff.hs | 54 +++++++++++++++++++++++++++++++++ src/TaskMachine/UI/Types.hs | 28 +++-------------- todo.txt | 9 +++--- 8 files changed, 152 insertions(+), 65 deletions(-) create mode 100644 src/TaskMachine/UI/Stuff.hs diff --git a/app/Main.hs b/app/Main.hs index e67a0d9..ad32d25 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -10,5 +10,4 @@ import TaskMachine.UI main :: IO() main = do o <- parseOptions - state <- loadTasks $ startUIState o - void $ B.defaultMain myApp state + void $ B.defaultMain myApp $ startUIState o diff --git a/src/TaskMachine/LTask.hs b/src/TaskMachine/LTask.hs index 1ad58a7..9045b57 100644 --- a/src/TaskMachine/LTask.hs +++ b/src/TaskMachine/LTask.hs @@ -20,10 +20,12 @@ module TaskMachine.LTask , saveLTasks ) where +import Control.Exception import Data.Function import Data.List +import System.IO.Error -import qualified Data.Vector as V +import qualified Data.Vector as V import Text.Megaparsec import TaskMachine.Task @@ -54,14 +56,45 @@ modifyLTask f (LTask pos task) = LTask pos (f task) sortLTasks :: [LTask] -> [LTask] sortLTasks = sortBy (compare `on` lPosition) +{- Loading -} + +data ErrorAction + = ErrorMessage String + | IgnoreError + deriving (Show) + +loadErrorMessage :: IOError -> Maybe ErrorAction +loadErrorMessage e + | isDoesNotExistError e = Just IgnoreError + | isIllegalOperation e = Just $ ErrorMessage $ "Could not open file:\n" ++ show e + | isPermissionError e = Just $ ErrorMessage "Could not open file: Permission denied" + | otherwise = Nothing + loadLTasks :: FilePath -> IO (Either String (V.Vector LTask)) loadLTasks file = do - content <- readFile file - case parse pTasks file content of - Right taskList -> pure $ Right $ V.fromList $ fromTasks taskList - Left parseError -> pure $ Left $ parseErrorPretty parseError + content <- tryJust loadErrorMessage $ readFile file + case parse pTasks file <$> content of + Left IgnoreError -> pure $ Right V.empty + Left (ErrorMessage msg) -> pure $ Left msg + Right (Left parseError) -> pure $ Left $ parseErrorPretty parseError + Right (Right taskList) -> pure $ Right $ V.fromList $ fromTasks taskList + --Left parseError -> pure $ Left $ parseErrorPretty parseError + --Right taskList -> pure $ Right $ V.fromList $ fromTasks taskList -saveLTasks :: FilePath -> V.Vector LTask -> IO () +{- Saving -} + +saveErrorMessage :: IOError -> Maybe String +saveErrorMessage e + | isAlreadyInUseError e = Just "Could not save to file: File already in use" + | isFullError e = Just "Could not save to file: Disk full" + | isIllegalOperation e = Just $ "Could not save to file:\n" ++ show e + | isPermissionError e = Just "Could not save to file: Permission denied" + | otherwise = Nothing + +saveLTasks :: FilePath -> V.Vector LTask -> IO (Either String ()) saveLTasks file ltasks = do let text = formatTasks $ toTasks $ V.toList ltasks - writeFile file text + result <- tryJust saveErrorMessage $ writeFile file text + case result of + Left ioErrorMessage -> pure $ Left ioErrorMessage + Right _ -> pure $ Right () diff --git a/src/TaskMachine/UI.hs b/src/TaskMachine/UI.hs index 12a13bf..3a935d9 100644 --- a/src/TaskMachine/UI.hs +++ b/src/TaskMachine/UI.hs @@ -1,16 +1,18 @@ module TaskMachine.UI ( myApp , startUIState - , loadTasks ) where import qualified Brick as B import qualified Brick.Themes as B +import Control.Monad.Trans import qualified Data.Vector as V import qualified Graphics.Vty.Input.Events as VTY import TaskMachine.Options import TaskMachine.UI.Behaviors +import TaskMachine.UI.Popup +import TaskMachine.UI.Stuff import TaskMachine.UI.TaskList import TaskMachine.UI.Types @@ -20,7 +22,7 @@ drawTaskList :: UIState -> B.Widget RName drawTaskList s = renderTaskList (taskEdit s) True (tasks s) drawUIState :: UIState -> [B.Widget RName] ---drawUIState s@UIState{errorPopup=Just p} = [renderPopupOk p, drawTaskList s] +drawUIState s@UIState{errorPopup=Just p} = [renderPopup p, drawTaskList s] drawUIState s = [drawTaskList s] {- Updating the state -} @@ -30,9 +32,19 @@ closeBehavior _ s (VTY.EvKey VTY.KEsc []) = B.halt s closeBehavior _ s (VTY.EvKey (VTY.KChar 'q') []) = B.halt s closeBehavior f s e = f s e -- wrapper around another behavior +popupBehavior :: Popup RName (UIState -> NewState) -> UIState -> VTY.Event -> NewState +popupBehavior p s (VTY.EvKey VTY.KEnter []) = + case popupSelection p of + Nothing -> B.continue s{errorPopup=Nothing} + Just action -> do + action s{errorPopup=Nothing} +popupBehavior p s e = do + newPopup <- handlePopupEvent e p + B.continue s{errorPopup=Just newPopup} + selectBehavior :: UIState -> VTY.Event -> NewState -- Deal with popup if there is one ---selectBehavior s@UIState{errorPopup=Just popup} e = undefined popup s e +selectBehavior s@UIState{errorPopup=Just p} e = popupBehavior p s e -- Under the assumption that tasks can only be edited while the task list is focused, edit a task selectBehavior s@UIState{taskEdit=Just edit} e = taskEditBehavior edit s e -- If nothing immediately jumps out at you, see which part has focus. @@ -44,19 +56,22 @@ updateUIState s _ = B.continue s {- Starting the app -} +startEvent :: UIState -> B.EventM RName UIState +startEvent = liftIO . loadTasks + myApp :: B.App UIState () RName myApp = B.App { B.appDraw = drawUIState , B.appChooseCursor = B.showFirstCursor , B.appHandleEvent = updateUIState - , B.appStartEvent = pure + , B.appStartEvent = startEvent , B.appAttrMap = const (B.themeToAttrMap defaultTheme) } startUIState :: Options -> UIState startUIState o = UIState - { options = o - --, errorPopup = Nothing - , tasks = taskList RTaskList V.empty - , taskEdit = Nothing + { options = o + , errorPopup = Nothing + , tasks = taskList RTaskList V.empty + , taskEdit = Nothing } diff --git a/src/TaskMachine/UI/Behaviors.hs b/src/TaskMachine/UI/Behaviors.hs index 5a76d7a..ccc56bb 100644 --- a/src/TaskMachine/UI/Behaviors.hs +++ b/src/TaskMachine/UI/Behaviors.hs @@ -13,6 +13,7 @@ import Text.Megaparsec import TaskMachine.Task import TaskMachine.UI.TaskList import TaskMachine.UI.Types +import TaskMachine.UI.Stuff startEdit :: UIState -> UIState startEdit s = @@ -36,8 +37,7 @@ taskEditBehavior _ s (VTY.EvKey VTY.KEsc []) = B.continue s{taskEdit=Nothin taskEditBehavior edit s (VTY.EvKey VTY.KHome []) = B.continue s{taskEdit=Just (B.applyEdit T.gotoBOL edit)} taskEditBehavior edit s (VTY.EvKey VTY.KEnd []) = B.continue s{taskEdit=Just (B.applyEdit T.gotoEOL edit)} taskEditBehavior edit s (VTY.EvKey VTY.KEnter []) = do - let newState = finishEdit edit s - liftIO $ saveTasks newState + newState <- liftIO $ saveTasks $ finishEdit edit s B.continue newState taskEditBehavior edit s e = do newEdit <- B.handleEditorEvent e edit @@ -45,9 +45,7 @@ taskEditBehavior edit s e = do taskListBehavior :: UIState -> VTY.Event -> NewState -- Reload while running -taskListBehavior s (VTY.EvKey (VTY.KChar 'r') []) = do - newState <- liftIO $ loadTasks s - B.continue newState +taskListBehavior s (VTY.EvKey (VTY.KChar 'r') []) = actionLoad s -- Mark/unmark a task as completed taskListBehavior s (VTY.EvKey (VTY.KChar 'x') []) = undefined s -- Delete tasks diff --git a/src/TaskMachine/UI/Popup.hs b/src/TaskMachine/UI/Popup.hs index 219e355..dab846c 100644 --- a/src/TaskMachine/UI/Popup.hs +++ b/src/TaskMachine/UI/Popup.hs @@ -1,11 +1,12 @@ module TaskMachine.UI.Popup ( minPopupWidth -- * Ok popup - , PopupOk - , popupOk - , popupOk' - , renderPopupOk - , handlePopupOkEvent + , Popup + , popup + , popup' + , renderPopup + , handlePopupEvent + , popupSelection ) where import qualified Brick as B @@ -17,18 +18,22 @@ minPopupWidth = 78 {- Ok popup -} -data PopupOk n = PopupOk (B.Dialog ()) (B.Widget n) +data Popup n r = Popup (B.Dialog r) (B.Widget n) -popupOk :: String -> String -> PopupOk n -popupOk title content = popupOk' title (B.str content) +popup :: String -> String -> [(String, r)] -> Popup n r +popup title content = popup' title (B.str content) -popupOk' :: String -> B.Widget n -> PopupOk n -popupOk' title widget = - let dialog = B.dialog (Just $ " " ++ title ++ " ") (Just (0,[("Ok",())])) minPopupWidth - in PopupOk dialog widget +popup' :: String -> B.Widget n -> [(String, r)] -> Popup n r +popup' title widget results = + let spacedTitle = " " ++ title ++ " " + dialog = B.dialog (Just spacedTitle) (Just (0, results)) minPopupWidth + in Popup dialog widget -renderPopupOk :: PopupOk n -> B.Widget n -renderPopupOk (PopupOk dialog widget) = B.renderDialog dialog widget +renderPopup :: Popup n r -> B.Widget n +renderPopup (Popup dialog widget) = B.renderDialog dialog widget -handlePopupOkEvent :: VTY.Event -> PopupOk n -> B.EventM n (PopupOk n) -handlePopupOkEvent e (PopupOk dialog widget) = PopupOk <$> B.handleDialogEvent e dialog <*> pure widget +handlePopupEvent :: VTY.Event -> Popup n r -> B.EventM n (Popup n r) +handlePopupEvent e (Popup dialog widget) = Popup <$> B.handleDialogEvent e dialog <*> pure widget + +popupSelection :: Popup n r -> Maybe r +popupSelection (Popup dialog _) = B.dialogSelection dialog diff --git a/src/TaskMachine/UI/Stuff.hs b/src/TaskMachine/UI/Stuff.hs new file mode 100644 index 0000000..75c85c5 --- /dev/null +++ b/src/TaskMachine/UI/Stuff.hs @@ -0,0 +1,54 @@ +module TaskMachine.UI.Stuff where + +import qualified Brick as B +import Control.Monad.Trans + +import TaskMachine.LTask +import TaskMachine.Options +import TaskMachine.UI.Popup +import TaskMachine.UI.TaskList +import TaskMachine.UI.Types + +actionQuit :: UIState -> NewState +actionQuit = B.halt + +actionDoNothing :: UIState -> NewState +actionDoNothing = B.continue + +actionLoad :: UIState -> NewState +actionLoad s = do + state <- liftIO $ loadTasks s + B.continue state + +loadTasks :: UIState -> IO UIState +loadTasks s = do + let file = oTodofile $ options s + result <- loadLTasks file + case result of + Right ltasks -> pure s{tasks=taskList RTaskList ltasks} + Left errorMessage -> + let p = popup "Error loading tasks" errorMessage + [ ("Retry", actionLoad) + , ("Quit", actionQuit) + ] + in pure s{errorPopup=Just p} + +actionSave :: UIState -> NewState +actionSave s = do + state <- liftIO $ saveTasks s + B.continue state + +saveTasks :: UIState -> IO UIState +saveTasks s = do + let filepath = oTodofile (options s) + ltasks = taskListElements (tasks s) + result <- saveLTasks filepath ltasks + case result of + Right _ -> pure s + Left errorMessage -> + let p = popup "Error saving tasks" errorMessage + [ ("Retry", actionSave) + , ("Continue without saving", actionDoNothing) + , ("Quit", actionQuit) + ] + in pure s{errorPopup=Just p} diff --git a/src/TaskMachine/UI/Types.hs b/src/TaskMachine/UI/Types.hs index 7500ea7..6a313f7 100644 --- a/src/TaskMachine/UI/Types.hs +++ b/src/TaskMachine/UI/Types.hs @@ -10,8 +10,6 @@ module TaskMachine.UI.Types , UIState(..) , NewState , defaultTheme - , loadTasks - , saveTasks ) where import qualified Brick as B @@ -21,8 +19,8 @@ import qualified Brick.Widgets.Edit as B import qualified Brick.Widgets.List as B import qualified Graphics.Vty as VTY -import TaskMachine.LTask import TaskMachine.Options +import TaskMachine.UI.Popup import TaskMachine.UI.Task import TaskMachine.UI.TaskList @@ -35,10 +33,10 @@ data RName {- UI state -} data UIState = UIState - { options :: Options -- includes todo file and other config - --, errorPopup :: Maybe (PopupOk RName) - , tasks :: TaskList RName - , taskEdit :: Maybe (B.Editor String RName) + { options :: Options -- includes todo file and other config + , errorPopup :: Maybe (Popup RName (UIState -> NewState)) + , tasks :: TaskList RName + , taskEdit :: Maybe (B.Editor String RName) } type NewState = B.EventM RName (B.Next UIState) @@ -73,19 +71,3 @@ defaultTheme = B.newTheme VTY.defAttr bg' = VTY.withBackColor none st' = VTY.withStyle none none = VTY.defAttr - -loadTasks :: UIState -> IO UIState -loadTasks s = do - let file = oTodofile $ options s - result <- loadLTasks file - case result of - -- TODO: Improve error handling when loading files - --Left errorMessage -> pure s{errorPopup=Just $ popupOk "Error loading tasks" errorMessage} - Left errorMessage -> undefined errorMessage - Right ltasks -> pure s{tasks=taskList RTaskList ltasks} - -saveTasks :: UIState -> IO () -saveTasks s = do - let filepath = oTodofile (options s) - ltasks = taskListElements (tasks s) - saveLTasks filepath ltasks diff --git a/todo.txt b/todo.txt index 4cb4270..ad3d01f 100644 --- a/todo.txt +++ b/todo.txt @@ -1,10 +1,11 @@ -- c2018-09-18 Offer "retry" or "quit" +dialogue - couldn't load from todo file -- c2018-09-18 Offer "retry" or "quit" +dialogue - couldn't save to todo file -- c2018-09-18 Offer "retry" or "quit" +dialogue - syntax error in todo file - c2018-09-18 Offer to clean up file when loading (adding creation/completion dates) - c2018-09-18 Purge - move completed tasks to a separate file -- c2018-09-18 Sort tasks by completion, priority, due date, description - c2018-09-28 Move cursor to beginning of task description when editing tasks - c2018-09-28 Syntax highlighting while editing tasks +- c2018-09-30 Sort tasks when (re-)loading x2018-09-27 c2018-09-18 Quit using Esc or q x2018-09-29 c2018-09-28 Use B.EventM's MonadIO instance instead of B.suspendAndResume (facepalm) +x2018-09-30 c2018-09-18 Offer "retry" or "quit" +dialogue - couldn't load from todo file +x2018-09-30 c2018-09-18 Offer "retry" or "quit" +dialogue - couldn't save to todo file +x2018-09-30 c2018-09-18 Offer "retry" or "quit" +dialogue - syntax error in todo file +x2018-09-30 c2018-09-30 Custom exception messages From 36e90895f0482384f7b0de428a5c587a43ab84fa Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 30 Sep 2018 21:39:50 +0000 Subject: [PATCH 12/28] Clean up --- src/TaskMachine/UI.hs | 5 ++--- src/TaskMachine/UI/Behaviors.hs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/TaskMachine/UI.hs b/src/TaskMachine/UI.hs index 3a935d9..26dcd52 100644 --- a/src/TaskMachine/UI.hs +++ b/src/TaskMachine/UI.hs @@ -35,9 +35,8 @@ closeBehavior f s e = f s e -- wrapper around anoth popupBehavior :: Popup RName (UIState -> NewState) -> UIState -> VTY.Event -> NewState popupBehavior p s (VTY.EvKey VTY.KEnter []) = case popupSelection p of - Nothing -> B.continue s{errorPopup=Nothing} - Just action -> do - action s{errorPopup=Nothing} + Nothing -> B.continue s{errorPopup=Nothing} + Just action -> action s{errorPopup=Nothing} popupBehavior p s e = do newPopup <- handlePopupEvent e p B.continue s{errorPopup=Just newPopup} diff --git a/src/TaskMachine/UI/Behaviors.hs b/src/TaskMachine/UI/Behaviors.hs index ccc56bb..73bbf53 100644 --- a/src/TaskMachine/UI/Behaviors.hs +++ b/src/TaskMachine/UI/Behaviors.hs @@ -11,9 +11,9 @@ import qualified Graphics.Vty as VTY import Text.Megaparsec import TaskMachine.Task +import TaskMachine.UI.Stuff import TaskMachine.UI.TaskList import TaskMachine.UI.Types -import TaskMachine.UI.Stuff startEdit :: UIState -> UIState startEdit s = From 15c547fe5e65b27a2d441fda88421105540de9c0 Mon Sep 17 00:00:00 2001 From: Joscha Date: Tue, 23 Oct 2018 13:30:32 +0000 Subject: [PATCH 13/28] Reorganize --- src/TaskMachine/Task.hs | 9 ++ src/TaskMachine/UI.hs | 35 ++---- src/TaskMachine/UI/Behaviors.hs | 216 +++++++++++++++++++++++++++----- src/TaskMachine/UI/Popup.hs | 13 +- src/TaskMachine/UI/Stuff.hs | 54 -------- src/TaskMachine/UI/TaskEdit.hs | 56 +++++++++ src/TaskMachine/UI/TaskList.hs | 121 +++++++++++------- src/TaskMachine/UI/Types.hs | 3 +- todo.txt | 2 +- 9 files changed, 343 insertions(+), 166 deletions(-) delete mode 100644 src/TaskMachine/UI/Stuff.hs create mode 100644 src/TaskMachine/UI/TaskEdit.hs diff --git a/src/TaskMachine/Task.hs b/src/TaskMachine/Task.hs index ae82c10..cc77027 100644 --- a/src/TaskMachine/Task.hs +++ b/src/TaskMachine/Task.hs @@ -21,6 +21,8 @@ module TaskMachine.Task , Description , Snippet(..) -- * Misc stuff + , emptyTask + , newTask , compareTasks -- * Formatting , formatTask @@ -313,6 +315,13 @@ pTasks = many pTask <* eof {- Misc stuff -} +emptyTask :: Task +emptyTask = Task Incomplete Nothing Nothing Nothing [] + +-- | Create a new task with empty description and the given date as creation date +newTask :: Day -> Task +newTask day = Task Incomplete Nothing Nothing (Just day) [] + compareTasks :: Task -> Task -> Ordering compareTasks a b = mconcat [ compare (taskCompletion a) (taskCompletion b) diff --git a/src/TaskMachine/UI.hs b/src/TaskMachine/UI.hs index 26dcd52..38b7048 100644 --- a/src/TaskMachine/UI.hs +++ b/src/TaskMachine/UI.hs @@ -5,21 +5,19 @@ module TaskMachine.UI import qualified Brick as B import qualified Brick.Themes as B -import Control.Monad.Trans import qualified Data.Vector as V import qualified Graphics.Vty.Input.Events as VTY import TaskMachine.Options import TaskMachine.UI.Behaviors import TaskMachine.UI.Popup -import TaskMachine.UI.Stuff import TaskMachine.UI.TaskList import TaskMachine.UI.Types {- Rendering -} drawTaskList :: UIState -> B.Widget RName -drawTaskList s = renderTaskList (taskEdit s) True (tasks s) +drawTaskList s = renderTaskList (editor s) True (tasks s) drawUIState :: UIState -> [B.Widget RName] drawUIState s@UIState{errorPopup=Just p} = [renderPopup p, drawTaskList s] @@ -27,27 +25,13 @@ drawUIState s = [drawTaskList s] {- Updating the state -} -closeBehavior :: (UIState -> VTY.Event -> NewState) -> UIState -> VTY.Event -> NewState -closeBehavior _ s (VTY.EvKey VTY.KEsc []) = B.halt s -closeBehavior _ s (VTY.EvKey (VTY.KChar 'q') []) = B.halt s -closeBehavior f s e = f s e -- wrapper around another behavior - -popupBehavior :: Popup RName (UIState -> NewState) -> UIState -> VTY.Event -> NewState -popupBehavior p s (VTY.EvKey VTY.KEnter []) = - case popupSelection p of - Nothing -> B.continue s{errorPopup=Nothing} - Just action -> action s{errorPopup=Nothing} -popupBehavior p s e = do - newPopup <- handlePopupEvent e p - B.continue s{errorPopup=Just newPopup} - selectBehavior :: UIState -> VTY.Event -> NewState -- Deal with popup if there is one -selectBehavior s@UIState{errorPopup=Just p} e = popupBehavior p s e --- Under the assumption that tasks can only be edited while the task list is focused, edit a task -selectBehavior s@UIState{taskEdit=Just edit} e = taskEditBehavior edit s e --- If nothing immediately jumps out at you, see which part has focus. -selectBehavior s e = closeBehavior taskListBehavior s e +selectBehavior s@UIState{errorPopup=Just p} e = closeModifier (popupBehavior p) s e +-- Continue editing task if previously editing a task +selectBehavior s@UIState{editor=Just edit} e = taskEditBehavior edit s e +-- Default task list behavior +selectBehavior s e = closeModifier taskListBehavior s e updateUIState :: UIState -> B.BrickEvent RName () -> NewState updateUIState s (B.VtyEvent e) = selectBehavior s e @@ -55,15 +39,12 @@ updateUIState s _ = B.continue s {- Starting the app -} -startEvent :: UIState -> B.EventM RName UIState -startEvent = liftIO . loadTasks - myApp :: B.App UIState () RName myApp = B.App { B.appDraw = drawUIState , B.appChooseCursor = B.showFirstCursor , B.appHandleEvent = updateUIState - , B.appStartEvent = startEvent + , B.appStartEvent = actionLoad , B.appAttrMap = const (B.themeToAttrMap defaultTheme) } @@ -72,5 +53,5 @@ startUIState o = UIState { options = o , errorPopup = Nothing , tasks = taskList RTaskList V.empty - , taskEdit = Nothing + , editor = Nothing } diff --git a/src/TaskMachine/UI/Behaviors.hs b/src/TaskMachine/UI/Behaviors.hs index 73bbf53..2f685c2 100644 --- a/src/TaskMachine/UI/Behaviors.hs +++ b/src/TaskMachine/UI/Behaviors.hs @@ -1,58 +1,212 @@ module TaskMachine.UI.Behaviors - ( taskListBehavior + ( Behavior + --, emptyBehavior + -- * Miscellaneous + , getCurrentDay + , closeModifier + -- * Behaviors + , popupBehavior + , taskListBehavior , taskEditBehavior + -- * Actions + , actionLoad + , actionSave + , actionDelete + , actionEditNew + , actionEditSelected + , actionSortTasks + , actionFinishEdit ) where -import qualified Brick as B -import qualified Brick.Widgets.Edit as B -import Control.Monad.Trans -import qualified Data.Text.Zipper as T -import qualified Graphics.Vty as VTY -import Text.Megaparsec +import Control.Monad +import qualified Brick as B +--import qualified Brick.Widgets.Edit as B +import Control.Monad.Trans +--import qualified Data.Text.Zipper as T +import qualified Graphics.Vty as VTY +--import Text.Megaparsec +import Data.Time + +import TaskMachine.LTask +import TaskMachine.Options import TaskMachine.Task -import TaskMachine.UI.Stuff +import TaskMachine.UI.Popup +import TaskMachine.UI.TaskEdit import TaskMachine.UI.TaskList import TaskMachine.UI.Types -startEdit :: UIState -> UIState -startEdit s = - case taskListSelectedElement (tasks s) of - Nothing -> undefined -- TODO: Add popup notification +type Behavior = UIState -> VTY.Event -> B.EventM RName (B.Next UIState) + +type Action = UIState -> B.EventM RName UIState + +{- Miscellaneous -} + +getCurrentDay :: IO Day +getCurrentDay = utctDay <$> liftIO getCurrentTime + +closeModifier :: Behavior -> Behavior +closeModifier _ s (VTY.EvKey VTY.KEsc []) = B.halt s +closeModifier _ s (VTY.EvKey (VTY.KChar 'q') []) = B.halt s +closeModifier f s e = f s e -- wrapper around another behavior + +{- Popups -} + +popupBehavior :: Popup RName (UIState -> NewState) -> Behavior +popupBehavior p s (VTY.EvKey VTY.KEnter []) = + case popupSelection p of + Nothing -> B.continue s{errorPopup=Nothing} -- Just close, no action was specified + Just action -> action s{errorPopup=Nothing} -- Do the thing! (and close the popup) +popupBehavior p s e = do + newPopup <- handlePopupEvent e p + B.continue s{errorPopup=Just newPopup} + +{- On the task list -} + +-- (re-)loading + +actionLoad :: Action +actionLoad s = do + let file = oTodofile $ options s + result <- liftIO $ loadLTasks file + case result of + Right ltasks -> pure s{tasks=taskList RTaskList ltasks} + Left errorMessage -> + let p = popup "Error loading tasks" errorMessage + [ ("Retry", actionLoad >=> B.continue) + , ("Quit", B.halt) + ] + in pure s{errorPopup=Just p} + +-- saving + +actionSave :: Action +actionSave s = do + let filepath = oTodofile (options s) + ltasks = taskListElements (tasks s) + result <- liftIO $ saveLTasks filepath ltasks + case result of + Right _ -> pure s + Left errorMessage -> + let p = popup "Error saving tasks" errorMessage + [ ("Retry", actionSave >=> B.continue) + , ("Continue without saving", B.continue) + , ("Quit", B.halt) + ] + in pure s{errorPopup=Just p} + +-- deleting a task + +actionDelete :: Action +actionDelete s = pure s{tasks=deleteTask (tasks s)} + +-- beginning an edit + +actionEditNew :: Action +actionEditNew s = do + today <- liftIO getCurrentDay + let task = newTask today + edit = taskEdit RTaskEdit task NewTask + pure s{editor=Just edit} + +actionEditSelected :: Action +actionEditSelected s = + case selectedTask (tasks s) of + Nothing -> error "no task selected" -- TODO: Add popup notification Just t -> - let edit = B.editor RTaskEdit (Just 1) (formatTask t) - in s{taskEdit=Just edit} + let edit = taskEdit RTaskEdit t ExistingTask + in pure s{editor=Just edit} -finishEdit :: B.Editor String RName -> UIState -> UIState +-- toggling completion + +actionToggleCompletion :: Action +actionToggleCompletion s = + case selectedTask (tasks s) of + Nothing -> pure s + Just task -> do + newCompletion <- case taskCompletion task of + Complete _ -> pure Incomplete + Incomplete -> Complete . Just <$> liftIO getCurrentDay + let task' = task{taskCompletion=newCompletion} + newTaskList = replaceTask task' (tasks s) + pure s{tasks=newTaskList} + +-- sorting + +actionSortTasks :: Action +actionSortTasks s = pure s{tasks=sortTaskList (tasks s)} + +-- combining all of the above... + +taskListBehavior :: Behavior +-- Clean up: Add todays date where creation/completion date is missing +--taskListBehavior s (VTY.EvKey (VTY.KChar 'c') []) = undefined s +-- Delete currently selected task (implicit save) +taskListBehavior s (VTY.EvKey (VTY.KChar 'd') []) = + actionDelete >=> actionSave >=> B.continue $ s +-- Begin editing currently selected task +taskListBehavior s (VTY.EvKey (VTY.KChar 'e') []) = + actionEditSelected >=> B.continue $ s +-- Begin creating new task +taskListBehavior s (VTY.EvKey (VTY.KChar 'n') []) = + actionEditNew >=> B.continue $ s +-- Reload tasks (and sort them) +taskListBehavior s (VTY.EvKey (VTY.KChar 'r') []) = + actionLoad >=> B.continue $ s +-- Sort tasks +taskListBehavior s (VTY.EvKey (VTY.KChar 's') []) = + actionSortTasks >=> B.continue $ s +-- Toggle completion (implicit save) +taskListBehavior s (VTY.EvKey (VTY.KChar 'x') []) = + actionToggleCompletion >=> actionSave >=> B.continue $ s +-- Update the task list (scroll etc.) +taskListBehavior s e = do + newTasks <- updateTaskList e (tasks s) + B.continue s{tasks=newTasks} + +{- In the task editor -} + +actionFinishEdit :: TaskEdit RName -> Action +actionFinishEdit t = pure . finishEdit t + +-- get result of task editing +-- if editing an existing task, modify that task +-- if editing a new task, append that task +finishEdit :: TaskEdit RName -> UIState -> UIState finishEdit edit s = - let editedText = unlines $ B.getEditContents edit - in case parse pTask "edited task" editedText of - Left parseError -> undefined parseError -- TODO: Add popup notification - Right newTask -> - let newTaskList = taskListModify (const newTask) (tasks s) - in s{tasks=newTaskList, taskEdit=Nothing} + case editedTask edit of + Left e -> error e -- TODO: Error popup + Right task -> s{tasks=modify task, editor=Nothing} + where + modify :: Task -> TaskList RName + modify task = case editState edit of + ExistingTask -> replaceTask task $ tasks s + NewTask -> appendTask task $ tasks s -taskEditBehavior :: B.Editor String RName -> UIState -> VTY.Event -> NewState -taskEditBehavior _ s (VTY.EvKey VTY.KEsc []) = B.continue s{taskEdit=Nothing} -taskEditBehavior edit s (VTY.EvKey VTY.KHome []) = B.continue s{taskEdit=Just (B.applyEdit T.gotoBOL edit)} -taskEditBehavior edit s (VTY.EvKey VTY.KEnd []) = B.continue s{taskEdit=Just (B.applyEdit T.gotoEOL edit)} -taskEditBehavior edit s (VTY.EvKey VTY.KEnter []) = do - newState <- liftIO $ saveTasks $ finishEdit edit s - B.continue newState +taskEditBehavior :: TaskEdit RName -> Behavior +taskEditBehavior _ s (VTY.EvKey VTY.KEsc []) = B.continue s{editor=Nothing} +taskEditBehavior edit s (VTY.EvKey VTY.KEnter []) = + actionFinishEdit edit >=> actionSave >=> B.continue $ s +--taskEditBehavior edit s (VTY.EvKey VTY.KEnter []) = do +-- newState <- liftIO $ saveTasks $ finishEdit edit s +-- B.continue newState taskEditBehavior edit s e = do - newEdit <- B.handleEditorEvent e edit - B.continue s{taskEdit=Just newEdit} + newEdit <- updateTaskEdit e edit + B.continue s{editor=Just newEdit} -taskListBehavior :: UIState -> VTY.Event -> NewState +{- -- Reload while running taskListBehavior s (VTY.EvKey (VTY.KChar 'r') []) = actionLoad s -- Mark/unmark a task as completed taskListBehavior s (VTY.EvKey (VTY.KChar 'x') []) = undefined s -- Delete tasks taskListBehavior s (VTY.EvKey (VTY.KChar 'd') []) = undefined s +-- Delete tasks +taskListBehavior s (VTY.EvKey (VTY.KChar 'd') []) = undefined s -- Start editing a new task taskListBehavior s (VTY.EvKey (VTY.KChar 'e') []) = B.continue (startEdit s) -- Update the task list (scroll etc.) taskListBehavior s e = do newTasks <- updateTaskList e (tasks s) B.continue s{tasks=newTasks} +-} diff --git a/src/TaskMachine/UI/Popup.hs b/src/TaskMachine/UI/Popup.hs index dab846c..d13cd5b 100644 --- a/src/TaskMachine/UI/Popup.hs +++ b/src/TaskMachine/UI/Popup.hs @@ -1,23 +1,17 @@ module TaskMachine.UI.Popup - ( minPopupWidth - -- * Ok popup - , Popup + ( Popup , popup , popup' , renderPopup , handlePopupEvent , popupSelection + , minPopupWidth ) where import qualified Brick as B import qualified Brick.Widgets.Dialog as B import qualified Graphics.Vty as VTY -minPopupWidth :: Int -minPopupWidth = 78 - -{- Ok popup -} - data Popup n r = Popup (B.Dialog r) (B.Widget n) popup :: String -> String -> [(String, r)] -> Popup n r @@ -37,3 +31,6 @@ handlePopupEvent e (Popup dialog widget) = Popup <$> B.handleDialogEvent e dialo popupSelection :: Popup n r -> Maybe r popupSelection (Popup dialog _) = B.dialogSelection dialog + +minPopupWidth :: Int +minPopupWidth = 78 diff --git a/src/TaskMachine/UI/Stuff.hs b/src/TaskMachine/UI/Stuff.hs deleted file mode 100644 index 75c85c5..0000000 --- a/src/TaskMachine/UI/Stuff.hs +++ /dev/null @@ -1,54 +0,0 @@ -module TaskMachine.UI.Stuff where - -import qualified Brick as B -import Control.Monad.Trans - -import TaskMachine.LTask -import TaskMachine.Options -import TaskMachine.UI.Popup -import TaskMachine.UI.TaskList -import TaskMachine.UI.Types - -actionQuit :: UIState -> NewState -actionQuit = B.halt - -actionDoNothing :: UIState -> NewState -actionDoNothing = B.continue - -actionLoad :: UIState -> NewState -actionLoad s = do - state <- liftIO $ loadTasks s - B.continue state - -loadTasks :: UIState -> IO UIState -loadTasks s = do - let file = oTodofile $ options s - result <- loadLTasks file - case result of - Right ltasks -> pure s{tasks=taskList RTaskList ltasks} - Left errorMessage -> - let p = popup "Error loading tasks" errorMessage - [ ("Retry", actionLoad) - , ("Quit", actionQuit) - ] - in pure s{errorPopup=Just p} - -actionSave :: UIState -> NewState -actionSave s = do - state <- liftIO $ saveTasks s - B.continue state - -saveTasks :: UIState -> IO UIState -saveTasks s = do - let filepath = oTodofile (options s) - ltasks = taskListElements (tasks s) - result <- saveLTasks filepath ltasks - case result of - Right _ -> pure s - Left errorMessage -> - let p = popup "Error saving tasks" errorMessage - [ ("Retry", actionSave) - , ("Continue without saving", actionDoNothing) - , ("Quit", actionQuit) - ] - in pure s{errorPopup=Just p} diff --git a/src/TaskMachine/UI/TaskEdit.hs b/src/TaskMachine/UI/TaskEdit.hs new file mode 100644 index 0000000..d4cb133 --- /dev/null +++ b/src/TaskMachine/UI/TaskEdit.hs @@ -0,0 +1,56 @@ +module TaskMachine.UI.TaskEdit + ( TaskEdit + , EditState(..) + , taskEdit + , editState + , editedTask + , renderTaskEdit + , updateTaskEdit + ) where + +import qualified Brick as B +import qualified Brick.Widgets.Edit as B +import qualified Data.Text.Zipper as T +import qualified Graphics.Vty as VTY +import Text.Megaparsec + +import TaskMachine.Task + +data TaskEdit n = TaskEdit EditState (B.Editor String n) + deriving (Show) + +data EditState = ExistingTask | NewTask + deriving (Show) + +taskEdit :: n -> Task -> EditState -> TaskEdit n +taskEdit name task s = TaskEdit s $ B.editor name (Just 1) (formatTask task) + +editState :: TaskEdit n -> EditState +editState (TaskEdit s _) = s + +editedLine :: TaskEdit n -> Either String String +editedLine (TaskEdit _ edit) = + case B.getEditContents edit of + [s] -> Right s + _ -> Left "Editor empty" + +editedTask :: TaskEdit n -> Either String Task +editedTask te = do + s <- editedLine te + case parse pTask "task editor" s of + Left parseError -> Left $ parseErrorPretty parseError + Right task -> Right task + +renderRows :: [String] -> B.Widget n +renderRows = B.vBox . map B.str + +renderTaskEdit :: (Ord n, Show n) => Bool -> TaskEdit n -> B.Widget n +renderTaskEdit focus (TaskEdit _ edit) = B.renderEditor renderRows focus edit + +updateTaskEdit :: Ord n => VTY.Event -> TaskEdit n -> B.EventM n (TaskEdit n) +updateTaskEdit (VTY.EvKey VTY.KHome []) (TaskEdit s edit) = + pure $ TaskEdit s $ B.applyEdit T.gotoBOL edit +updateTaskEdit (VTY.EvKey VTY.KEnd []) (TaskEdit s edit) = + pure $ TaskEdit s $ B.applyEdit T.gotoEOL edit +updateTaskEdit event (TaskEdit s edit) = + TaskEdit s <$> B.handleEditorEvent event edit diff --git a/src/TaskMachine/UI/TaskList.hs b/src/TaskMachine/UI/TaskList.hs index 4423904..2db48a6 100644 --- a/src/TaskMachine/UI/TaskList.hs +++ b/src/TaskMachine/UI/TaskList.hs @@ -1,63 +1,96 @@ module TaskMachine.UI.TaskList ( TaskList , taskList + , taskListElements , renderTaskList , updateTaskList - , taskListElements - , taskListFilter - , taskListSelectedElement - , taskListModify + , sortTaskList + , selectedTask + , appendTask + , replaceTask + , deleteTask ) where -import qualified Brick as B -import qualified Brick.Widgets.Edit as B -import qualified Brick.Widgets.List as B -import qualified Data.Vector as V -import qualified Graphics.Vty as VTY +import Data.Function +import Data.List + +import qualified Brick as B +import qualified Brick.Widgets.List as B +import qualified Data.Vector as V +import qualified Graphics.Vty as VTY import TaskMachine.LTask import TaskMachine.Task import TaskMachine.UI.Task +import TaskMachine.UI.TaskEdit -data TaskList n = TaskList - { visibleTasks :: B.List n LTask - , invisibleTasks :: V.Vector LTask - } deriving (Show) - -newList :: n -> V.Vector LTask -> B.List n LTask -newList name ltasks = B.list name ltasks 1 +newtype TaskList n = TaskList (B.List n LTask) taskList :: n -> V.Vector LTask -> TaskList n -taskList name ltasks = TaskList{visibleTasks=newList name ltasks, invisibleTasks=V.empty} - -renderLTask :: (Ord n, Show n) => Maybe (B.Editor String n) -> Bool -> LTask -> B.Widget n -renderLTask (Just e) True _ = B.renderEditor (B.str . unlines) True e -renderLTask _ _ lt = renderTask (toTask lt) - -renderTaskList :: (Ord n, Show n) => Maybe (B.Editor String n) -> Bool -> TaskList n -> B.Widget n -renderTaskList edit focus tl = B.renderList (renderLTask edit) focus (visibleTasks tl) - -updateTaskList :: (Ord n) => VTY.Event -> TaskList n -> B.EventM n (TaskList n) -updateTaskList e tl = do - updatedList <- B.handleListEventVi B.handleListEvent e (visibleTasks tl) - pure tl{visibleTasks=updatedList} - -{- Managing tasks -} +taskList name tasks = TaskList $ B.list name tasks 1 taskListElements :: TaskList n -> V.Vector LTask -taskListElements tl = B.listElements (visibleTasks tl) <> invisibleTasks tl +taskListElements (TaskList list) = B.listElements list -taskListFilter :: (Task -> Bool) -> TaskList n -> TaskList n -taskListFilter f tl = - let (yes, no) = V.partition (f . toTask) $ taskListElements tl - name = B.listName (visibleTasks tl) - list = newList name yes - in TaskList{visibleTasks=list, invisibleTasks=no} +renderRow :: Maybe (B.Widget n) -> Bool -> LTask -> B.Widget n +renderRow (Just w) True _ = w +renderRow _ _ lt = renderTask (toTask lt) -taskListSelectedElement :: TaskList n -> Maybe Task -taskListSelectedElement tl = toTask . snd <$> B.listSelectedElement (visibleTasks tl) +renderLast :: (Ord n, Show n) => B.Widget n -> Bool -> B.List n LTask -> B.Widget n +renderLast widget focus list = + let listWithPlaceholder = focusOnLastTask $ appendTask' emptyTask list + in B.renderList (renderRow (Just widget)) focus listWithPlaceholder -taskListModify :: (Task -> Task) -> TaskList n -> TaskList n -taskListModify f tl = - let list = B.listModify (modifyLTask f) (visibleTasks tl) - in tl{visibleTasks=list} +renderTaskList :: (Ord n, Show n) => Maybe (TaskEdit n) -> Bool -> TaskList n -> B.Widget n +renderTaskList Nothing focus (TaskList list) + | listSize list == 0 = renderLast (B.str "--- empty ---") focus list + | otherwise = B.renderList (renderRow Nothing) focus list +renderTaskList (Just te) focus (TaskList list) = + case editState te of + ExistingTask -> B.renderList (renderRow (Just teWidget)) focus list + NewTask -> renderLast teWidget focus list + where + teWidget = renderTaskEdit focus te + +updateTaskList :: Ord n => VTY.Event -> TaskList n -> B.EventM n (TaskList n) +updateTaskList event (TaskList list) = + TaskList <$> B.handleListEventVi B.handleListEvent event list + +sortTaskList :: TaskList n -> TaskList n +sortTaskList (TaskList list) = + let tasks = V.toList $ B.listElements list + sortedTasks = sortBy (compareTasks `on` toTask) tasks + newVector = V.fromList sortedTasks + in TaskList $ B.listReplace newVector Nothing list + +selectedTask :: TaskList n -> Maybe Task +selectedTask (TaskList list) = toTask . snd <$> B.listSelectedElement list + +appendTask' :: Task -> B.List n LTask -> B.List n LTask +appendTask' task list = + let size = listSize list + lt = lTask task + in focusOnLastTask $ B.listInsert size lt list + +appendTask :: Task -> TaskList n -> TaskList n +appendTask task (TaskList list) = TaskList $ appendTask' task list + +replaceTask :: Task -> TaskList n -> TaskList n +replaceTask task (TaskList list) = TaskList $ B.listModify replace list + where + replace :: LTask -> LTask + replace = modifyLTask (const task) + +deleteTask :: TaskList n -> TaskList n +deleteTask tl@(TaskList list) = + case listSize list of + 0 -> tl + n -> TaskList $ B.listRemove (n-1) list + +{- helper functions -} + +listSize :: B.List n e -> Int +listSize list = V.length $ B.listElements list + +focusOnLastTask :: B.List n e -> B.List n e +focusOnLastTask list = B.listMoveTo (listSize list - 1) list diff --git a/src/TaskMachine/UI/Types.hs b/src/TaskMachine/UI/Types.hs index 6a313f7..f39235d 100644 --- a/src/TaskMachine/UI/Types.hs +++ b/src/TaskMachine/UI/Types.hs @@ -22,6 +22,7 @@ import qualified Graphics.Vty as VTY import TaskMachine.Options import TaskMachine.UI.Popup import TaskMachine.UI.Task +import TaskMachine.UI.TaskEdit import TaskMachine.UI.TaskList -- | Resource names @@ -36,7 +37,7 @@ data UIState = UIState { options :: Options -- includes todo file and other config , errorPopup :: Maybe (Popup RName (UIState -> NewState)) , tasks :: TaskList RName - , taskEdit :: Maybe (B.Editor String RName) + , editor :: Maybe (TaskEdit RName) } type NewState = B.EventM RName (B.Next UIState) diff --git a/todo.txt b/todo.txt index ad3d01f..eecbfce 100644 --- a/todo.txt +++ b/todo.txt @@ -2,7 +2,7 @@ - c2018-09-18 Purge - move completed tasks to a separate file - c2018-09-28 Move cursor to beginning of task description when editing tasks - c2018-09-28 Syntax highlighting while editing tasks -- c2018-09-30 Sort tasks when (re-)loading +x2018-10-23 c2018-09-30 Display "-empty-" when TaskList is empty x2018-09-27 c2018-09-18 Quit using Esc or q x2018-09-29 c2018-09-28 Use B.EventM's MonadIO instance instead of B.suspendAndResume (facepalm) x2018-09-30 c2018-09-18 Offer "retry" or "quit" +dialogue - couldn't load from todo file From 038721177d5df14ef1121c7613de1c921e3402c6 Mon Sep 17 00:00:00 2001 From: Joscha Date: Tue, 23 Oct 2018 13:46:43 +0000 Subject: [PATCH 14/28] Move cursor to description when editing --- src/TaskMachine/Task.hs | 31 +++++++++++++++++++++---------- src/TaskMachine/UI/TaskEdit.hs | 8 +++++++- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/TaskMachine/Task.hs b/src/TaskMachine/Task.hs index cc77027..53e500a 100644 --- a/src/TaskMachine/Task.hs +++ b/src/TaskMachine/Task.hs @@ -26,6 +26,7 @@ module TaskMachine.Task , compareTasks -- * Formatting , formatTask + , formatTaskHalves , formatTasks , formatDate , formatDue @@ -73,6 +74,23 @@ data Task = Task , taskDescription :: Description } deriving (Show) +-- | Convert a 'Task' to its string representation. +-- This string representation is split into a pre-description and a description part. +-- +-- For further detail, see 'formatTask' +formatTaskHalves :: Task -> (String, String) +formatTaskHalves t = + ( formatCompletion (taskCompletion t) ++ " " + ++ maybeWithSpace formatPriority (taskPriority t) + ++ maybeWithSpace formatDue(taskDue t) + ++ maybeWithSpace formatCreated (taskCreated t) + , formatDescription (taskDescription t) + ) + where + maybeWithSpace :: (a -> String) -> Maybe a -> String + maybeWithSpace _ Nothing = "" + maybeWithSpace f (Just a) = f a ++ " " + -- | Convert a 'Task' to its string representation, which can be parsed by 'pTask'. -- -- If this string representation is parsed using 'pTask', it should yield the original task, @@ -81,16 +99,9 @@ data Task = Task -- could include the text version of these in the beginning, i. e. @taskDescription = "(A) hello"@. -- In that case, converting the task to a string and back yields a different result. formatTask :: Task -> String -formatTask t - = formatCompletion (taskCompletion t) ++ " " - ++ maybeWithSpace formatPriority (taskPriority t) - ++ maybeWithSpace formatDue(taskDue t) - ++ maybeWithSpace formatCreated (taskCreated t) - ++ formatDescription (taskDescription t) - where - maybeWithSpace :: (a -> String) -> Maybe a -> String - maybeWithSpace _ Nothing = "" - maybeWithSpace f (Just a) = f a ++ " " +formatTask t = + let (predesc, desc) = formatTaskHalves t + in predesc ++ desc -- | Convert a list of tasks to its string representation, which can be parsed by 'pTasks'. formatTasks :: [Task] -> String diff --git a/src/TaskMachine/UI/TaskEdit.hs b/src/TaskMachine/UI/TaskEdit.hs index d4cb133..fcf018b 100644 --- a/src/TaskMachine/UI/TaskEdit.hs +++ b/src/TaskMachine/UI/TaskEdit.hs @@ -23,7 +23,13 @@ data EditState = ExistingTask | NewTask deriving (Show) taskEdit :: n -> Task -> EditState -> TaskEdit n -taskEdit name task s = TaskEdit s $ B.editor name (Just 1) (formatTask task) +taskEdit name task s = + let (predesc, desc) = formatTaskHalves task + formattedTask = predesc ++ desc + cursor = length predesc + editor = B.editor name (Just 1) formattedTask + newEditor = B.applyEdit (T.moveCursor (0, cursor)) editor + in TaskEdit s newEditor editState :: TaskEdit n -> EditState editState (TaskEdit s _) = s From 05f4343d40d2855219f7b4db17335f1f674b36ef Mon Sep 17 00:00:00 2001 From: Joscha Date: Tue, 23 Oct 2018 14:13:05 +0000 Subject: [PATCH 15/28] Prevent cursor from moving when deleting tasks --- src/TaskMachine/UI/TaskList.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/TaskMachine/UI/TaskList.hs b/src/TaskMachine/UI/TaskList.hs index 2db48a6..3164f36 100644 --- a/src/TaskMachine/UI/TaskList.hs +++ b/src/TaskMachine/UI/TaskList.hs @@ -83,9 +83,9 @@ replaceTask task (TaskList list) = TaskList $ B.listModify replace list deleteTask :: TaskList n -> TaskList n deleteTask tl@(TaskList list) = - case listSize list of - 0 -> tl - n -> TaskList $ B.listRemove (n-1) list + case B.listSelected list of + Nothing -> tl + Just index -> TaskList $ B.listMoveBy 1 $ B.listRemove index list {- helper functions -} From 54e192aa8c4a50a225a13364fe1df2a11d965a20 Mon Sep 17 00:00:00 2001 From: Joscha Date: Tue, 23 Oct 2018 15:31:24 +0000 Subject: [PATCH 16/28] Change syntax highlighting --- src/TaskMachine/UI/Task.hs | 5 ++++- src/TaskMachine/UI/Types.hs | 2 +- todo.txt | 5 +++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/TaskMachine/UI/Task.hs b/src/TaskMachine/UI/Task.hs index 5a2c311..01fc34e 100644 --- a/src/TaskMachine/UI/Task.hs +++ b/src/TaskMachine/UI/Task.hs @@ -30,7 +30,10 @@ withSpace :: B.Widget n -> B.Widget n withSpace w = w B.<+> B.withDefAttr taskAttr (B.str " ") renderCompletion :: Completion -> B.Widget n -renderCompletion = B.withDefAttr taskCompletionAttr . B.str . formatCompletion +renderCompletion Incomplete = B.str "-" +renderCompletion (Complete Nothing) = B.str "x" +renderCompletion (Complete (Just day)) = + B.str "x" B.<+> B.withDefAttr taskCompletionAttr (B.str $ formatDate day) renderPriority :: Priority -> B.Widget n renderPriority p = diff --git a/src/TaskMachine/UI/Types.hs b/src/TaskMachine/UI/Types.hs index f39235d..93af906 100644 --- a/src/TaskMachine/UI/Types.hs +++ b/src/TaskMachine/UI/Types.hs @@ -53,7 +53,7 @@ defaultTheme = B.newTheme VTY.defAttr , (B.listSelectedAttr, st' VTY.bold) , (B.listSelectedFocusedAttr, bg VTY.blue $ st' VTY.bold) , (taskAttr, none) - , (taskCompletionAttr, none) + , (taskCompletionAttr, fg' VTY.brightBlack) , (taskPriorityAttr, fg VTY.cyan $ st' VTY.bold) , (taskPriorityAttr <> "A", fg VTY.red $ st' VTY.bold) , (taskPriorityAttr <> "B", fg VTY.yellow $ st' VTY.bold) diff --git a/todo.txt b/todo.txt index eecbfce..af28786 100644 --- a/todo.txt +++ b/todo.txt @@ -1,6 +1,6 @@ -- c2018-09-18 Offer to clean up file when loading (adding creation/completion dates) +- c2018-09-18 Clean up file (adding creation/completion dates) - c2018-09-18 Purge - move completed tasks to a separate file -- c2018-09-28 Move cursor to beginning of task description when editing tasks +x2018-10-23 c2018-09-28 Move cursor to beginning of task description when editing tasks - c2018-09-28 Syntax highlighting while editing tasks x2018-10-23 c2018-09-30 Display "-empty-" when TaskList is empty x2018-09-27 c2018-09-18 Quit using Esc or q @@ -9,3 +9,4 @@ x2018-09-30 c2018-09-18 Offer "retry" or "quit" +dialogue - couldn't load from t x2018-09-30 c2018-09-18 Offer "retry" or "quit" +dialogue - couldn't save to todo file x2018-09-30 c2018-09-18 Offer "retry" or "quit" +dialogue - syntax error in todo file x2018-09-30 c2018-09-30 Custom exception messages +- c2018-10-23 Don't crash on parse errors etc. From 04dbb364ccd9b3ddf0536481ebc4ddddefabbbdf Mon Sep 17 00:00:00 2001 From: Joscha Date: Tue, 23 Oct 2018 15:36:21 +0000 Subject: [PATCH 17/28] Fix cursor movement when deleting tasks --- src/TaskMachine/UI/TaskList.hs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/TaskMachine/UI/TaskList.hs b/src/TaskMachine/UI/TaskList.hs index 3164f36..7966082 100644 --- a/src/TaskMachine/UI/TaskList.hs +++ b/src/TaskMachine/UI/TaskList.hs @@ -85,7 +85,9 @@ deleteTask :: TaskList n -> TaskList n deleteTask tl@(TaskList list) = case B.listSelected list of Nothing -> tl - Just index -> TaskList $ B.listMoveBy 1 $ B.listRemove index list + Just index + | index == 0 -> TaskList $ B.listRemove index list + | otherwise -> TaskList $ B.listMoveBy 1 $ B.listRemove index list {- helper functions -} From bac127e82c3e86691b2ddb36f57850eb5fe22fc8 Mon Sep 17 00:00:00 2001 From: Joscha Date: Tue, 23 Oct 2018 16:23:19 +0000 Subject: [PATCH 18/28] Add missing dates to tasks --- src/TaskMachine/UI/Behaviors.hs | 21 ++++++++++++++++++++- src/TaskMachine/UI/TaskList.hs | 13 +++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/TaskMachine/UI/Behaviors.hs b/src/TaskMachine/UI/Behaviors.hs index 2f685c2..ea7b0c6 100644 --- a/src/TaskMachine/UI/Behaviors.hs +++ b/src/TaskMachine/UI/Behaviors.hs @@ -14,6 +14,7 @@ module TaskMachine.UI.Behaviors , actionDelete , actionEditNew , actionEditSelected + , actionToggleCompletion , actionSortTasks , actionFinishEdit ) where @@ -136,11 +137,29 @@ actionToggleCompletion s = actionSortTasks :: Action actionSortTasks s = pure s{tasks=sortTaskList (tasks s)} +-- cleaning up tasks + +cleanUpTask :: Day -> Task -> Task +cleanUpTask today (Task (Complete Nothing) p d Nothing desc) = + Task (Complete (Just today)) p d (Just today) desc +cleanUpTask today (Task (Complete Nothing) p d c desc) = + Task (Complete (Just today)) p d c desc +cleanUpTask today (Task c p d Nothing desc) = + Task c p d (Just today) desc +cleanUpTask _ t = t + +actionCleanUp :: Action +actionCleanUp s = do + today <- liftIO getCurrentDay + let tasks' = modifyAllTasks (cleanUpTask today) (tasks s) + pure s{tasks=tasks'} + -- combining all of the above... taskListBehavior :: Behavior -- Clean up: Add todays date where creation/completion date is missing ---taskListBehavior s (VTY.EvKey (VTY.KChar 'c') []) = undefined s +taskListBehavior s (VTY.EvKey (VTY.KChar 'c') []) = + actionCleanUp >=> actionSave >=> B.continue $ s -- Delete currently selected task (implicit save) taskListBehavior s (VTY.EvKey (VTY.KChar 'd') []) = actionDelete >=> actionSave >=> B.continue $ s diff --git a/src/TaskMachine/UI/TaskList.hs b/src/TaskMachine/UI/TaskList.hs index 7966082..4e7e6f8 100644 --- a/src/TaskMachine/UI/TaskList.hs +++ b/src/TaskMachine/UI/TaskList.hs @@ -9,6 +9,7 @@ module TaskMachine.UI.TaskList , appendTask , replaceTask , deleteTask + , modifyAllTasks ) where import Data.Function @@ -58,10 +59,11 @@ updateTaskList event (TaskList list) = sortTaskList :: TaskList n -> TaskList n sortTaskList (TaskList list) = - let tasks = V.toList $ B.listElements list + let index = B.listSelected list + tasks = V.toList $ B.listElements list sortedTasks = sortBy (compareTasks `on` toTask) tasks newVector = V.fromList sortedTasks - in TaskList $ B.listReplace newVector Nothing list + in TaskList $ B.listReplace newVector index list selectedTask :: TaskList n -> Maybe Task selectedTask (TaskList list) = toTask . snd <$> B.listSelectedElement list @@ -89,6 +91,13 @@ deleteTask tl@(TaskList list) = | index == 0 -> TaskList $ B.listRemove index list | otherwise -> TaskList $ B.listMoveBy 1 $ B.listRemove index list +modifyAllTasks :: (Task -> Task) -> TaskList n -> TaskList n +modifyAllTasks f (TaskList list) = + let index = B.listSelected list + vector = B.listElements list + vector' = V.map (modifyLTask f) vector + in TaskList $ B.listReplace vector' index list + {- helper functions -} listSize :: B.List n e -> Int From c7f67945d8a8ed75fe8c06b8f92e76f9fa95fee6 Mon Sep 17 00:00:00 2001 From: Joscha Date: Tue, 23 Oct 2018 16:59:30 +0000 Subject: [PATCH 19/28] Remove wrong information --- README.md | 65 ++----------------------------------------------------- 1 file changed, 2 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index bf17779..dabe8dc 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,5 @@ # task-machine -A TUI client for the [todo.txt](https://github.com/todotxt/todo.txt) format, written in Haskell, -that supports automatically creating new tasks based on template tasks. +A TUI client for the a format inspired by [todo.txt](https://github.com/todotxt/todo.txt), written in Haskell. -For an introduction to the file format, see the [todo.txt readme](https://github.com/todotxt/todo.txt/blob/master/README.md). - -## Template tasks - -Template tasks allow for automatically creating new tasks on certain dates. - -They work by evaluating a formula for the current day. -If the formula evaluates to `true`, a new task gets added to the current day using the template text specified. -If it evaluates to `false`, nothing happens. - -When creating a template task, a start date can be specified. -In that case, the formula will be evaluated for every day from the starting date to the current date. - -Once all tasks are added, the starting date will be set to one day after the current date automatically, -to avoid evaluating any day twice, even if no starting date was specified initially. - -Template tasks are inspired by Ben Crowell's [when](https://github.com/bcrowell/when) calendar program. - -### Format - -This is the format for template tasks. -The square brackets `[]` indicate an optional part of the syntax. -The curly braces `{}` are part of the syntax. - -`# [start-date] {when-formula} [priority] template-text` - -- The `#` signals that this line is a template task, not a normal task. - -- *Optional*: All days from `start-date` to the current date are tested when creating new tasks. -It should be in the `YYYY-MM-DD` format. -If no `start-date` is specified, it is assumed to be the current date. - -- The `when-formula` is evaluated in order to decide whether a new task should be created for a day. - -- *Optional*: The `priority` is a regular todo.txt priority, for example `(E)`. - -- The `template-text` is pretty much a normal task description. -The only difference is that it supports date expressions. - -#### Examples - -`# 2018-09-08 {day_of_week == fri} clean the kitchen @home` - -`# {d2018-10-20 - today >= 0} (A) daily every day up to (and including) 2018-10-20 with priority A` - -`# {day == 20 && month == 10} Tom's birthday ({year - 1978} years old)` -(if Tom was born on 1978-10-20) - -### Date expression - -***TODO***: Document properly once it's actually implemented - -- *date* + - *number* → *date* -- *date* - *date* → *number* -- *number* + - \* / *number* → *number* -- *number* == != > < >= <= *number* → *boolean* -- *boolean* && || == != *boolean* → *boolean* - -### When formula - -### Template text +Still in development... From c557b89e46ed076f7af13d8631e480ac14b2545a Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 24 Oct 2018 16:54:21 +0000 Subject: [PATCH 20/28] Replace crashes with popups --- src/TaskMachine/UI/Behaviors.hs | 9 +++++++-- todo.txt | 7 +++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/TaskMachine/UI/Behaviors.hs b/src/TaskMachine/UI/Behaviors.hs index ea7b0c6..f7aa265 100644 --- a/src/TaskMachine/UI/Behaviors.hs +++ b/src/TaskMachine/UI/Behaviors.hs @@ -113,7 +113,7 @@ actionEditNew s = do actionEditSelected :: Action actionEditSelected s = case selectedTask (tasks s) of - Nothing -> error "no task selected" -- TODO: Add popup notification + Nothing -> pure s Just t -> let edit = taskEdit RTaskEdit t ExistingTask in pure s{editor=Just edit} @@ -194,7 +194,12 @@ actionFinishEdit t = pure . finishEdit t finishEdit :: TaskEdit RName -> UIState -> UIState finishEdit edit s = case editedTask edit of - Left e -> error e -- TODO: Error popup + Left e -> + let p = popup "Syntax error" e + [ ("Continue editing", B.continue) + , ("Abort", \s' -> B.continue s'{editor=Nothing}) + ] + in s{errorPopup=Just p} Right task -> s{tasks=modify task, editor=Nothing} where modify :: Task -> TaskList RName diff --git a/todo.txt b/todo.txt index af28786..555e400 100644 --- a/todo.txt +++ b/todo.txt @@ -1,4 +1,4 @@ -- c2018-09-18 Clean up file (adding creation/completion dates) +x2018-10-23 c2018-09-18 Clean up file (adding creation/completion dates) - c2018-09-18 Purge - move completed tasks to a separate file x2018-10-23 c2018-09-28 Move cursor to beginning of task description when editing tasks - c2018-09-28 Syntax highlighting while editing tasks @@ -9,4 +9,7 @@ x2018-09-30 c2018-09-18 Offer "retry" or "quit" +dialogue - couldn't load from t x2018-09-30 c2018-09-18 Offer "retry" or "quit" +dialogue - couldn't save to todo file x2018-09-30 c2018-09-18 Offer "retry" or "quit" +dialogue - syntax error in todo file x2018-09-30 c2018-09-30 Custom exception messages -- c2018-10-23 Don't crash on parse errors etc. +x2018-10-24 c2018-10-23 Don't crash on parse errors etc. +- c2018-10-24 Add "u" - undo and "z" - redo buttons +- c2018-10-24 Redo coloring - maybe black-on-white color scheme in-editor? +- c2018-10-24 Reverse ordering of completed tasks - most recent at the top From 3e0a9d34f7f00b617b85693c58f8b78bd862d8fa Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 24 Oct 2018 16:56:38 +0000 Subject: [PATCH 21/28] Clean up --- src/TaskMachine/UI/Behaviors.hs | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/TaskMachine/UI/Behaviors.hs b/src/TaskMachine/UI/Behaviors.hs index f7aa265..d592ae4 100644 --- a/src/TaskMachine/UI/Behaviors.hs +++ b/src/TaskMachine/UI/Behaviors.hs @@ -1,6 +1,5 @@ module TaskMachine.UI.Behaviors ( Behavior - --, emptyBehavior -- * Miscellaneous , getCurrentDay , closeModifier @@ -22,12 +21,9 @@ module TaskMachine.UI.Behaviors import Control.Monad import qualified Brick as B ---import qualified Brick.Widgets.Edit as B import Control.Monad.Trans ---import qualified Data.Text.Zipper as T -import qualified Graphics.Vty as VTY ---import Text.Megaparsec import Data.Time +import qualified Graphics.Vty as VTY import TaskMachine.LTask import TaskMachine.Options @@ -217,20 +213,3 @@ taskEditBehavior edit s (VTY.EvKey VTY.KEnter []) = taskEditBehavior edit s e = do newEdit <- updateTaskEdit e edit B.continue s{editor=Just newEdit} - -{- --- Reload while running -taskListBehavior s (VTY.EvKey (VTY.KChar 'r') []) = actionLoad s --- Mark/unmark a task as completed -taskListBehavior s (VTY.EvKey (VTY.KChar 'x') []) = undefined s --- Delete tasks -taskListBehavior s (VTY.EvKey (VTY.KChar 'd') []) = undefined s --- Delete tasks -taskListBehavior s (VTY.EvKey (VTY.KChar 'd') []) = undefined s --- Start editing a new task -taskListBehavior s (VTY.EvKey (VTY.KChar 'e') []) = B.continue (startEdit s) --- Update the task list (scroll etc.) -taskListBehavior s e = do - newTasks <- updateTaskList e (tasks s) - B.continue s{tasks=newTasks} --} From bac45278fe234f4081518fdd55dc4c69ecd54418 Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 24 Oct 2018 17:19:00 +0000 Subject: [PATCH 22/28] Reverse ordering of completed tasks --- src/TaskMachine/Task.hs | 25 ++++++++++++++++--------- todo.txt | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/TaskMachine/Task.hs b/src/TaskMachine/Task.hs index 53e500a..eece150 100644 --- a/src/TaskMachine/Task.hs +++ b/src/TaskMachine/Task.hs @@ -333,19 +333,26 @@ emptyTask = Task Incomplete Nothing Nothing Nothing [] newTask :: Day -> Task newTask day = Task Incomplete Nothing Nothing (Just day) [] +-- Inverted compare for Maybes: Nothing comes after Just +compareMaybe :: Ord a => Maybe a -> Maybe a -> Ordering +compareMaybe Nothing Nothing = EQ +compareMaybe (Just _) Nothing = LT +compareMaybe Nothing (Just _) = GT +compareMaybe (Just x) (Just y) = compare x y + +compareDescription :: Description -> Description -> Ordering +compareDescription = compare `on` formatDescription + compareTasks :: Task -> Task -> Ordering +compareTasks a@(Task (Complete _) _ _ _ _) b@(Task (Complete _) _ _ _ _) = mconcat + [ compare (taskCompletion b) (taskCompletion a) + , compareMaybe (taskPriority a) (taskPriority b) + , compareMaybe (taskDue a) (taskDue b) + , compareDescription (taskDescription a) (taskDescription b) + ] compareTasks a b = mconcat [ compare (taskCompletion a) (taskCompletion b) , compareMaybe (taskPriority a) (taskPriority b) , compareMaybe (taskDue a) (taskDue b) , compareDescription (taskDescription a) (taskDescription b) ] - where - -- Inverted compare for Maybes: Nothing comes after Just - compareMaybe :: Ord a => Maybe a -> Maybe a -> Ordering - compareMaybe Nothing Nothing = EQ - compareMaybe (Just _) Nothing = LT - compareMaybe Nothing (Just _) = GT - compareMaybe (Just x) (Just y) = compare x y - compareDescription :: Description -> Description -> Ordering - compareDescription = compare `on` formatDescription diff --git a/todo.txt b/todo.txt index 555e400..d6c6610 100644 --- a/todo.txt +++ b/todo.txt @@ -12,4 +12,4 @@ x2018-09-30 c2018-09-30 Custom exception messages x2018-10-24 c2018-10-23 Don't crash on parse errors etc. - c2018-10-24 Add "u" - undo and "z" - redo buttons - c2018-10-24 Redo coloring - maybe black-on-white color scheme in-editor? -- c2018-10-24 Reverse ordering of completed tasks - most recent at the top +x2018-10-24 c2018-10-24 Reverse ordering of completed tasks - most recent at the top From b84f439c6c6dc6fedeef97076591e2b2042c4041 Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 24 Oct 2018 17:30:12 +0000 Subject: [PATCH 23/28] Add editor syntax highlighting --- src/TaskMachine/UI/TaskEdit.hs | 17 ++++++++++++----- todo.txt | 4 ++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/TaskMachine/UI/TaskEdit.hs b/src/TaskMachine/UI/TaskEdit.hs index fcf018b..0ebea6b 100644 --- a/src/TaskMachine/UI/TaskEdit.hs +++ b/src/TaskMachine/UI/TaskEdit.hs @@ -8,13 +8,14 @@ module TaskMachine.UI.TaskEdit , updateTaskEdit ) where -import qualified Brick as B -import qualified Brick.Widgets.Edit as B -import qualified Data.Text.Zipper as T -import qualified Graphics.Vty as VTY +import qualified Brick as B +import qualified Brick.Widgets.Edit as B +import qualified Data.Text.Zipper as T +import qualified Graphics.Vty as VTY import Text.Megaparsec import TaskMachine.Task +import TaskMachine.UI.Task data TaskEdit n = TaskEdit EditState (B.Editor String n) deriving (Show) @@ -47,8 +48,14 @@ editedTask te = do Left parseError -> Left $ parseErrorPretty parseError Right task -> Right task +renderRow :: String -> B.Widget n +renderRow s = + case parse pTask "" s of + Left _ -> B.str s + Right task -> renderTask task + renderRows :: [String] -> B.Widget n -renderRows = B.vBox . map B.str +renderRows = B.vBox . map renderRow renderTaskEdit :: (Ord n, Show n) => Bool -> TaskEdit n -> B.Widget n renderTaskEdit focus (TaskEdit _ edit) = B.renderEditor renderRows focus edit diff --git a/todo.txt b/todo.txt index d6c6610..d30151a 100644 --- a/todo.txt +++ b/todo.txt @@ -1,7 +1,7 @@ x2018-10-23 c2018-09-18 Clean up file (adding creation/completion dates) - c2018-09-18 Purge - move completed tasks to a separate file x2018-10-23 c2018-09-28 Move cursor to beginning of task description when editing tasks -- c2018-09-28 Syntax highlighting while editing tasks +x2018-10-24 c2018-09-28 Syntax highlighting while editing tasks x2018-10-23 c2018-09-30 Display "-empty-" when TaskList is empty x2018-09-27 c2018-09-18 Quit using Esc or q x2018-09-29 c2018-09-28 Use B.EventM's MonadIO instance instead of B.suspendAndResume (facepalm) @@ -11,5 +11,5 @@ x2018-09-30 c2018-09-18 Offer "retry" or "quit" +dialogue - syntax error in todo x2018-09-30 c2018-09-30 Custom exception messages x2018-10-24 c2018-10-23 Don't crash on parse errors etc. - c2018-10-24 Add "u" - undo and "z" - redo buttons -- c2018-10-24 Redo coloring - maybe black-on-white color scheme in-editor? +- c2018-10-24 Fix date coloring in editor x2018-10-24 c2018-10-24 Reverse ordering of completed tasks - most recent at the top From 08511840089b3d674d9c5b5f4b79b31e4d5da77e Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 24 Oct 2018 19:39:36 +0000 Subject: [PATCH 24/28] Add example screenshot --- README.md | 2 ++ example_screenshot.png | Bin 0 -> 5651 bytes 2 files changed, 2 insertions(+) create mode 100644 example_screenshot.png diff --git a/README.md b/README.md index dabe8dc..98589e7 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,5 @@ A TUI client for the a format inspired by [todo.txt](https://github.com/todotxt/todo.txt), written in Haskell. Still in development... + +![exampletodo.txt displayed using i3 and urxvt](example_screenshot.png) diff --git a/example_screenshot.png b/example_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..197a17088a00edefbf7d4c2043c70c9915270c98 GIT binary patch literal 5651 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 Date: Wed, 24 Oct 2018 20:05:18 +0000 Subject: [PATCH 25/28] Update resolver --- stack.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack.yaml b/stack.yaml index b0252a3..d4c68d1 100644 --- a/stack.yaml +++ b/stack.yaml @@ -15,7 +15,7 @@ # resolver: # name: custom-snapshot # location: "./custom-snapshot.yaml" -resolver: lts-12.9 +resolver: lts-12.14 # User packages to be built. # Various formats can be used as shown in the example below. From 67e4c9823203a3a7d36450689a102709fce63553 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 26 Oct 2018 20:43:55 +0000 Subject: [PATCH 26/28] Update todo.txt --- todo.txt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/todo.txt b/todo.txt index d30151a..5bb40d2 100644 --- a/todo.txt +++ b/todo.txt @@ -10,6 +10,14 @@ x2018-09-30 c2018-09-18 Offer "retry" or "quit" +dialogue - couldn't save to tod x2018-09-30 c2018-09-18 Offer "retry" or "quit" +dialogue - syntax error in todo file x2018-09-30 c2018-09-30 Custom exception messages x2018-10-24 c2018-10-23 Don't crash on parse errors etc. -- c2018-10-24 Add "u" - undo and "z" - redo buttons - c2018-10-24 Fix date coloring in editor x2018-10-24 c2018-10-24 Reverse ordering of completed tasks - most recent at the top +- c2018-10-24 F1 - list keybindings +- c2018-10-26 Add Z - undo (also Ctrl + Z) +- c2018-10-26 Add Y - redo (also Ctrl + Y) +- c2018-10-26 Implement date expressions using GADTs +- c2018-10-26 Move C - cleanup to F - fixup +- c2018-10-26 Add "starting" task type (start date, prio, due date, description) +- c2018-10-26 Add "formula" task type (formula, prio, due date, description) - maybe due date formula? +- (D) c2018-10-26 Fix crash on attempting to open a folder +- c2018-10-26 Think of something to do with +project tags, @context tags and key:value tags From 8c9de5cd8bc5f69228d43e8859f0f62f3492c80f Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 1 Nov 2018 17:53:35 +0000 Subject: [PATCH 27/28] Catch all io exceptions --- src/TaskMachine/LTask.hs | 13 ++----------- todo.txt | 2 +- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/TaskMachine/LTask.hs b/src/TaskMachine/LTask.hs index 9045b57..c2ae64d 100644 --- a/src/TaskMachine/LTask.hs +++ b/src/TaskMachine/LTask.hs @@ -66,9 +66,7 @@ data ErrorAction loadErrorMessage :: IOError -> Maybe ErrorAction loadErrorMessage e | isDoesNotExistError e = Just IgnoreError - | isIllegalOperation e = Just $ ErrorMessage $ "Could not open file:\n" ++ show e - | isPermissionError e = Just $ ErrorMessage "Could not open file: Permission denied" - | otherwise = Nothing + | otherwise = Just $ ErrorMessage $ show e loadLTasks :: FilePath -> IO (Either String (V.Vector LTask)) loadLTasks file = do @@ -78,18 +76,11 @@ loadLTasks file = do Left (ErrorMessage msg) -> pure $ Left msg Right (Left parseError) -> pure $ Left $ parseErrorPretty parseError Right (Right taskList) -> pure $ Right $ V.fromList $ fromTasks taskList - --Left parseError -> pure $ Left $ parseErrorPretty parseError - --Right taskList -> pure $ Right $ V.fromList $ fromTasks taskList {- Saving -} saveErrorMessage :: IOError -> Maybe String -saveErrorMessage e - | isAlreadyInUseError e = Just "Could not save to file: File already in use" - | isFullError e = Just "Could not save to file: Disk full" - | isIllegalOperation e = Just $ "Could not save to file:\n" ++ show e - | isPermissionError e = Just "Could not save to file: Permission denied" - | otherwise = Nothing +saveErrorMessage e = Just $ show e saveLTasks :: FilePath -> V.Vector LTask -> IO (Either String ()) saveLTasks file ltasks = do diff --git a/todo.txt b/todo.txt index 5bb40d2..393be5b 100644 --- a/todo.txt +++ b/todo.txt @@ -19,5 +19,5 @@ x2018-10-24 c2018-10-24 Reverse ordering of completed tasks - most recent at the - c2018-10-26 Move C - cleanup to F - fixup - c2018-10-26 Add "starting" task type (start date, prio, due date, description) - c2018-10-26 Add "formula" task type (formula, prio, due date, description) - maybe due date formula? -- (D) c2018-10-26 Fix crash on attempting to open a folder +x2018-11-01 (D) c2018-10-26 Fix crash on attempting to open a folder - c2018-10-26 Think of something to do with +project tags, @context tags and key:value tags From 59b2d64a3c95da87b9c8e2df10244e2043359c79 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 11 Jan 2019 23:01:08 +0000 Subject: [PATCH 28/28] Add uncommitted files --- plan.txt | 24 +++++++++++++ src/TaskMachine/History.hs | 72 ++++++++++++++++++++++++++++++++++++++ test.txt | 7 ++++ 3 files changed, 103 insertions(+) create mode 100644 plan.txt create mode 100644 src/TaskMachine/History.hs create mode 100644 test.txt diff --git a/plan.txt b/plan.txt new file mode 100644 index 0000000..fd7b02d --- /dev/null +++ b/plan.txt @@ -0,0 +1,24 @@ +normal task + - | x(completion date) + (priority) + (due date) + (creation date) + description + +delayed task + (start date) + (priority) + (due date) + (creation date) + description + + on update: + if after start date: + turn into normal task + +formula task + (when formula) + (priority) + (due date) + +"update": diff --git a/src/TaskMachine/History.hs b/src/TaskMachine/History.hs new file mode 100644 index 0000000..b27da08 --- /dev/null +++ b/src/TaskMachine/History.hs @@ -0,0 +1,72 @@ +-- | An undo-redo history that keeps a copy of every state. + +module TaskMachine.History + ( History + , history + , step + , current + , modify + , undo + , maybeUndo + , redo + , maybeRedo + ) where + +data Step a b = Step a b + +-- | Represents the history (only one branch) of some type @a@. +-- +-- Contains backwards (ctrl+z) as well as forwards (ctrl+y or ctrl+shift+z) history, +-- as well as the current state. +data History a b = History [Step a b] a [Step a b] + +-- | Create a new 'History' from a single state. +history :: a -> History a b +history a = History [] a [] + +-- | Add a new step to the history. +-- +-- Any forwards history will be overwritten, as this action starts a new +-- branch in the history tree. +step :: a -> b -> History a b -> History a b +step a (History xs y _) = History (Step y b : xs) a [] + +-- | Read the current state of the history. +current :: History a -> b -> a +current (History _ a _) = a + +{- +-- | Modify the current state, adding a step in the process. +-- +-- @'modify' f h = 'step' (f $ 'current' h) h@ +modify :: (a -> a) -> History a -> History a -- not a functor! +modify f h = step (f $ current h) h +-} + +-- | Jump to the previous state, remembering the future for later redos. +-- +-- If there is no previous state, do nothing. +undo :: History a b -> Maybe (History a b, b) +undo (History (Step x b : xs) y zs) = Just (History xs x (Step y b : zs), b) +undo _ = Nothing + +{- +-- | Like 'undo', but returns 'Nothing' if there was no previous state. +maybeUndo :: History a -> Maybe (History a) +maybeUndo (History (x:xs) y zs) = Just $ History xs x (y:zs) +maybeUndo _ = Nothing +-} + +-- | Jump to the next state, remembering the past for later undos. +-- +-- If there is no next state, do nothing. +redo :: History a b -> Maybe (History a b, b) +redo (History xs y (Step z b : zs)) = Just (History (Step y b : xs) z zs, b) +redo _ = Nothing + +{- +-- | Like 'redo', but returns 'Nothing' if there was no next state. +maybeRedo :: History a -> Maybe (History a) +maybeRedo (History xs y (z:zs)) = Just $ History (y:xs) z zs +maybeRedo _ = Nothing +-} diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..3e67d29 --- /dev/null +++ b/test.txt @@ -0,0 +1,7 @@ +- A simple task +x A simple completed task + +- c2018-12-13 A simple task with creation date + +s2018-12-20 d2018-12-24 A task with start date +-2018-12-20 d2018-12-24 A task with alternate start date