2024-05-26 23:21:15 +02:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2024-05-25 20:07:02 +02:00
|
|
|
|
2024-05-25 22:36:59 +02:00
|
|
|
module Main(main, parseTask, serializeTask, getPriority, comparePriority, TaskMap) where
|
2024-05-25 20:07:02 +02:00
|
|
|
|
|
|
|
import System.IO
|
|
|
|
import System.Directory
|
2024-05-26 17:47:23 +02:00
|
|
|
import Data.Text (Text, pack, unpack, isInfixOf, lines, unlines, strip, splitOn)
|
2024-05-25 20:07:02 +02:00
|
|
|
import qualified Data.Text.IO as TIO
|
2024-05-25 22:36:59 +02:00
|
|
|
import qualified Data.ByteString as BS
|
|
|
|
import qualified Data.ByteString.Char8 as BSC
|
2024-05-25 20:07:02 +02:00
|
|
|
import Control.Monad (when)
|
|
|
|
import Data.List (sortBy)
|
|
|
|
import Data.Ord (comparing)
|
|
|
|
import Data.Char (isAlpha)
|
2024-05-25 21:09:43 +02:00
|
|
|
import qualified Data.Map as Map
|
|
|
|
import Data.Function (on)
|
2024-05-26 17:47:23 +02:00
|
|
|
import Control.Exception (catch, IOException)
|
2024-05-25 20:07:02 +02:00
|
|
|
|
|
|
|
todoFile :: FilePath
|
|
|
|
todoFile = "todo.txt"
|
|
|
|
|
2024-05-25 21:09:43 +02:00
|
|
|
type TaskMap = Map.Map Int Text
|
|
|
|
|
2024-05-25 20:07:02 +02:00
|
|
|
main :: IO ()
|
|
|
|
main = do
|
|
|
|
putStrLn "Welcome to the Haskell Todo App"
|
|
|
|
putStrLn "1. Display tasks"
|
|
|
|
putStrLn "2. Add task"
|
2024-05-26 22:39:58 +02:00
|
|
|
putStrLn "3. Add task (Manual)"
|
2024-05-26 23:20:11 +02:00
|
|
|
putStrLn "4. Edit task"
|
|
|
|
putStrLn "5. Filter tasks by tag"
|
|
|
|
putStrLn "6. Sort tasks by priority"
|
|
|
|
putStrLn "7. Remove task"
|
|
|
|
putStrLn "8. Exit"
|
2024-05-25 20:07:02 +02:00
|
|
|
putStr "Choose an option: "
|
|
|
|
hFlush stdout
|
|
|
|
option <- getLine
|
|
|
|
case option of
|
|
|
|
"1" -> displayTasks
|
|
|
|
"2" -> addTask
|
2024-05-26 22:39:58 +02:00
|
|
|
"3" -> addTaskWithManual
|
2024-05-26 23:20:11 +02:00
|
|
|
"4" -> editTask
|
|
|
|
"5" -> filterTasksByTag
|
|
|
|
"6" -> sortTasksByPriority
|
|
|
|
"7" -> removeTask
|
|
|
|
"8" -> putStrLn "Goodbye!"
|
2024-05-25 20:07:02 +02:00
|
|
|
_ -> putStrLn "Invalid option" >> main
|
|
|
|
|
2024-05-26 23:20:11 +02:00
|
|
|
|
2024-05-25 20:07:02 +02:00
|
|
|
displayTasks :: IO ()
|
|
|
|
displayTasks = do
|
|
|
|
exists <- doesFileExist todoFile
|
|
|
|
if exists
|
|
|
|
then do
|
2024-05-26 17:47:23 +02:00
|
|
|
contents <- catch (TIO.readFile todoFile) handleReadError
|
|
|
|
let taskMap = parseTask contents
|
2024-05-25 20:07:02 +02:00
|
|
|
putStrLn "Tasks:"
|
2024-05-25 21:09:43 +02:00
|
|
|
mapM_ (putStrLn . showTask) (Map.toList taskMap)
|
2024-05-25 20:07:02 +02:00
|
|
|
else putStrLn "No tasks found."
|
2024-05-26 22:39:58 +02:00
|
|
|
putStrLn " -------------- "
|
|
|
|
putStrLn "Press Enter"
|
|
|
|
option <- getLine
|
|
|
|
case option of
|
|
|
|
_ -> main
|
|
|
|
where
|
|
|
|
handleReadError :: IOException -> IO Text
|
|
|
|
handleReadError e = do
|
|
|
|
putStrLn $ "Error reading file: " ++ show e
|
|
|
|
return ""
|
2024-05-25 20:07:02 +02:00
|
|
|
|
|
|
|
addTask :: IO ()
|
|
|
|
addTask = do
|
|
|
|
putStr "Enter the task: "
|
|
|
|
hFlush stdout
|
|
|
|
task <- getLine
|
2024-05-25 21:09:43 +02:00
|
|
|
exists <- doesFileExist todoFile
|
|
|
|
if exists
|
|
|
|
then do
|
2024-05-26 17:47:23 +02:00
|
|
|
contents <- catch (TIO.readFile todoFile) handleReadError
|
|
|
|
let taskMap = parseTask contents
|
2024-05-25 21:09:43 +02:00
|
|
|
newId = if Map.null taskMap then 1 else fst (Map.findMax taskMap) + 1
|
|
|
|
newTaskMap = Map.insert newId (pack task) taskMap
|
2024-05-26 17:47:23 +02:00
|
|
|
catch (TIO.writeFile todoFile (serializeTask newTaskMap)) handleWriteError
|
|
|
|
else catch (TIO.writeFile todoFile (pack task <> "\n")) handleWriteError
|
2024-05-25 20:07:02 +02:00
|
|
|
putStrLn "Task added."
|
2024-05-26 22:39:58 +02:00
|
|
|
putStrLn " -------------- "
|
|
|
|
putStrLn "Press Enter"
|
|
|
|
option <- getLine
|
|
|
|
case option of
|
|
|
|
_ -> main
|
2024-05-26 17:47:23 +02:00
|
|
|
where
|
|
|
|
handleReadError :: IOException -> IO Text
|
|
|
|
handleReadError e = do
|
|
|
|
putStrLn $ "Error reading file: " ++ show e
|
|
|
|
return ""
|
|
|
|
|
|
|
|
handleWriteError :: IOException -> IO ()
|
|
|
|
handleWriteError e = putStrLn $ "Error writing file: " ++ show e
|
2024-05-25 20:07:02 +02:00
|
|
|
|
2024-05-26 22:39:58 +02:00
|
|
|
addTaskWithManual :: IO ()
|
|
|
|
addTaskWithManual = do
|
|
|
|
putStrLn "First, optionally you can add priority to the task you want to add. To do so, put letter inside '()'"
|
|
|
|
putStrLn "Tasks without priority will be displayed first."
|
|
|
|
putStrLn "Second, write your task."
|
|
|
|
putStrLn "Third, optionally you can add tags to your task. To do so, put '+'and write your tag."
|
|
|
|
putStrLn "Tasks can have more than one tag."
|
|
|
|
putStrLn "Example: (C) go shopping +shop +sunday"
|
|
|
|
putStrLn " -------------- "
|
|
|
|
putStrLn "Press Enter to add your task or x to go back to menu"
|
|
|
|
option <- getLine
|
|
|
|
case option of
|
|
|
|
"x" -> main
|
|
|
|
_ -> addTask
|
|
|
|
|
2024-05-26 23:20:11 +02:00
|
|
|
|
|
|
|
editTask :: IO ()
|
|
|
|
editTask = do
|
|
|
|
exists <- doesFileExist todoFile
|
|
|
|
if exists
|
|
|
|
then do
|
|
|
|
contents <- catch (TIO.readFile todoFile) handleReadError
|
|
|
|
let taskMap = parseTask contents
|
|
|
|
putStrLn "Existing tasks: "
|
|
|
|
mapM_ (putStrLn . showTaskId) (Map.toList taskMap)
|
|
|
|
|
|
|
|
putStr "Enter the ID of the task you wish to edit: "
|
|
|
|
hFlush stdout
|
|
|
|
taskIdStr <- getLine
|
|
|
|
let taskId = read taskIdStr :: Int
|
|
|
|
|
|
|
|
putStrLn ""
|
|
|
|
|
|
|
|
if Map.member taskId taskMap
|
|
|
|
then do
|
|
|
|
putStrLn "Enter the new task description: "
|
|
|
|
newTask <- getLine
|
|
|
|
let newTaskMap = Map.insert taskId (pack newTask) taskMap
|
|
|
|
catch (TIO.writeFile todoFile (serializeTask newTaskMap)) handleWriteError
|
|
|
|
putStrLn "Task edited."
|
|
|
|
else do
|
|
|
|
putStrLn "Task of this ID doesn't exist"
|
|
|
|
putStrLn "Do you wish to try again? Press Enter if yes or x to go back to menu"
|
|
|
|
option <- getLine
|
|
|
|
case option of
|
|
|
|
"x" -> main
|
|
|
|
_ -> editTask
|
|
|
|
|
|
|
|
else putStrLn "No tasks found."
|
|
|
|
putStrLn " -------------- "
|
|
|
|
putStrLn "Press Enter"
|
|
|
|
option <- getLine
|
|
|
|
case option of
|
|
|
|
_ -> main
|
|
|
|
where
|
|
|
|
handleReadError :: IOException -> IO Text
|
|
|
|
handleReadError e = do
|
|
|
|
putStrLn $ "Error reading file: " ++ show e
|
|
|
|
return ""
|
|
|
|
|
|
|
|
handleWriteError :: IOException -> IO ()
|
|
|
|
handleWriteError e = putStrLn $ "Error writing file: " ++ show e
|
|
|
|
|
|
|
|
|
2024-05-25 20:07:02 +02:00
|
|
|
filterTasksByTag :: IO ()
|
|
|
|
filterTasksByTag = do
|
2024-05-25 21:09:43 +02:00
|
|
|
putStr "Enter the tag to filter by (e.g., +GarageSale): "
|
2024-05-25 20:07:02 +02:00
|
|
|
hFlush stdout
|
|
|
|
tag <- getLine
|
|
|
|
exists <- doesFileExist todoFile
|
|
|
|
if exists
|
|
|
|
then do
|
2024-05-26 17:47:23 +02:00
|
|
|
contents <- catch (TIO.readFile todoFile) handleReadError
|
2024-05-25 22:26:37 +02:00
|
|
|
let taskMap = parseTask contents
|
2024-05-25 21:09:43 +02:00
|
|
|
filteredTasks = Map.filter (isInfixOf (pack tag)) taskMap
|
2024-05-25 20:07:02 +02:00
|
|
|
putStrLn "Filtered tasks:"
|
2024-05-25 21:09:43 +02:00
|
|
|
mapM_ (putStrLn . showTask) (Map.toList filteredTasks)
|
2024-05-25 20:07:02 +02:00
|
|
|
else putStrLn "No tasks found."
|
2024-05-26 22:39:58 +02:00
|
|
|
putStrLn " -------------- "
|
|
|
|
putStrLn "Press Enter"
|
|
|
|
option <- getLine
|
|
|
|
case option of
|
|
|
|
_ -> main
|
2024-05-26 17:47:23 +02:00
|
|
|
where
|
|
|
|
handleReadError :: IOException -> IO Text
|
|
|
|
handleReadError e = do
|
|
|
|
putStrLn $ "Error reading file: " ++ show e
|
|
|
|
return ""
|
2024-05-25 20:07:02 +02:00
|
|
|
|
2024-05-26 22:39:58 +02:00
|
|
|
handleWriteError :: IOException -> IO ()
|
|
|
|
handleWriteError e = putStrLn $ "Error writing file: " ++ show e
|
|
|
|
|
2024-05-25 20:07:02 +02:00
|
|
|
sortTasksByPriority :: IO ()
|
|
|
|
sortTasksByPriority = do
|
|
|
|
exists <- doesFileExist todoFile
|
|
|
|
if exists
|
|
|
|
then do
|
2024-05-26 17:47:23 +02:00
|
|
|
contents <- catch (TIO.readFile todoFile) handleReadError
|
2024-05-25 22:26:37 +02:00
|
|
|
let taskMap = parseTask contents
|
2024-05-25 21:09:43 +02:00
|
|
|
sortedTasks = sortBy (comparePriority `on` snd) (Map.toList taskMap)
|
2024-05-25 20:07:02 +02:00
|
|
|
putStrLn "Sorted tasks by priority:"
|
2024-05-25 21:09:43 +02:00
|
|
|
mapM_ (putStrLn . showTask) sortedTasks
|
2024-05-25 20:07:02 +02:00
|
|
|
else putStrLn "No tasks found."
|
2024-05-26 22:39:58 +02:00
|
|
|
putStrLn " -------------- "
|
|
|
|
putStrLn "Press Enter"
|
|
|
|
option <- getLine
|
|
|
|
case option of
|
|
|
|
_ -> main
|
|
|
|
where
|
|
|
|
handleReadError :: IOException -> IO Text
|
|
|
|
handleReadError e = do
|
|
|
|
putStrLn $ "Error reading file: " ++ show e
|
|
|
|
return ""
|
|
|
|
|
|
|
|
removeTask :: IO ()
|
|
|
|
removeTask = do
|
|
|
|
|
|
|
|
exists <- doesFileExist todoFile
|
|
|
|
if exists
|
|
|
|
then do
|
|
|
|
contents <- catch (TIO.readFile todoFile) handleReadError
|
|
|
|
let taskMap = parseTask contents
|
|
|
|
putStrLn "Existing tasks: "
|
|
|
|
mapM_ (putStrLn . showTaskId) (Map.toList taskMap)
|
|
|
|
|
|
|
|
putStr "Enter the ID of the task you wish to remove: "
|
|
|
|
hFlush stdout
|
|
|
|
taskIdStr <- getLine
|
|
|
|
let taskId = read taskIdStr :: Int
|
|
|
|
|
|
|
|
putStrLn ""
|
|
|
|
|
|
|
|
if Map.member taskId taskMap
|
|
|
|
then do
|
|
|
|
let newTaskMap = Map.delete taskId taskMap
|
|
|
|
catch (TIO.writeFile todoFile (serializeTask newTaskMap)) handleWriteError
|
|
|
|
putStrLn "Task removed."
|
|
|
|
else do
|
|
|
|
putStrLn "Task of this ID doesn't exist"
|
|
|
|
putStrLn "Do you wish to try again? Press Enter if yes or x to go back to menu"
|
|
|
|
option <- getLine
|
|
|
|
case option of
|
|
|
|
"x" -> main
|
|
|
|
_ -> removeTask
|
|
|
|
|
|
|
|
else putStrLn "No tasks found."
|
|
|
|
putStrLn " -------------- "
|
|
|
|
putStrLn "Press Enter"
|
|
|
|
option <- getLine
|
|
|
|
case option of
|
|
|
|
_ -> main
|
2024-05-26 17:47:23 +02:00
|
|
|
where
|
|
|
|
handleReadError :: IOException -> IO Text
|
|
|
|
handleReadError e = do
|
|
|
|
putStrLn $ "Error reading file: " ++ show e
|
|
|
|
return ""
|
2024-05-25 20:07:02 +02:00
|
|
|
|
2024-05-26 22:39:58 +02:00
|
|
|
handleWriteError :: IOException -> IO ()
|
|
|
|
handleWriteError e = putStrLn $ "Error writing file: " ++ show e
|
|
|
|
|
|
|
|
|
|
|
|
|
2024-05-25 20:07:02 +02:00
|
|
|
comparePriority :: Text -> Text -> Ordering
|
|
|
|
comparePriority t1 t2 = comparing getPriority t1 t2
|
|
|
|
|
|
|
|
getPriority :: Text -> Maybe Char
|
2024-05-25 21:09:43 +02:00
|
|
|
getPriority t = case unpack (strip t) of
|
2024-05-25 20:07:02 +02:00
|
|
|
('(':p:')':_) | isAlpha p -> Just p
|
2024-05-25 21:09:43 +02:00
|
|
|
_ -> Nothing
|
|
|
|
|
2024-05-25 22:26:37 +02:00
|
|
|
parseTask :: Text -> TaskMap
|
|
|
|
parseTask = Map.fromList . zip [1..] . Data.Text.lines
|
2024-05-25 21:09:43 +02:00
|
|
|
|
2024-05-25 22:26:37 +02:00
|
|
|
serializeTask :: TaskMap -> Text
|
|
|
|
serializeTask = Data.Text.unlines . map snd . Map.toList
|
2024-05-25 21:09:43 +02:00
|
|
|
|
|
|
|
showTask :: (Int, Text) -> String
|
2024-05-26 22:39:58 +02:00
|
|
|
showTask (_, task) = unpack task
|
|
|
|
|
|
|
|
showTaskId :: (Int, Text) -> String
|
|
|
|
showTaskId (taskId, task) = show taskId ++ ": " ++ unpack task
|