#r "nuget:Argu" open System open System.IO open System.Text.RegularExpressions open Argu type TaskData = { Note: string } type Task = | DoneTask of TaskData | TodoTask of TaskData override x.ToString() = match x with | TodoTask {Note = note} -> $"- [ ] {note}" | DoneTask {Note = note} -> $"- [x] {note}" module Task = let create note = TodoTask({Note = note}) let check task = match task with | TodoTask(data) -> DoneTask(data) | _ -> task let undo task = match task with | DoneTask(data) -> TodoTask(data) | _ -> task let (|Parse|) str = let matches = Regex.Match(str, @"^- \[([\sx])\] (.*)$") if matches.Success then match [for i in matches.Groups -> i.Value] with | [_; " "; note] -> Some (create note) | [_; "x"; note] -> Some (DoneTask {Note = note}) | _ -> None else None type TodoList = { FilePath: string Todos: Task list } let tryDo (acion: (unit -> 'r)) () = try Ok (acion()) with | ex -> Error ex type StreamReader with member x.ReadAllLines() = [while not x.EndOfStream do x.ReadLine()] module TodoList = let load path = let file = File.Open(path, FileMode.OpenOrCreate) use reader = new StreamReader(file) let todos = reader.ReadAllLines() |> List.map ((function | Task.Parse (Some t) -> t | _ -> failwith "failed to parse tasks")) {FilePath = path; Todos = todos} let private save todos = let lines = todos.Todos |> List.map (fun t -> t.ToString()) File.WriteAllLines(todos.FilePath, lines) let private trySave todos = tryDo (fun () -> save todos todos) let add todos note = let added = {todos with Todos = todos.Todos @ [Task.create note]} trySave added let private update todos index acion = let idx = index - 1 let updated = acion todos.Todos[idx] let results = {todos with Todos = todos.Todos |> List.updateAt idx updated} trySave results let check todos index = update todos index Task.check let undo todos index = update todos index Task.undo let remove todos index = let idx = index - 1 let results = {todos with Todos = todos.Todos |> List.removeAt idx} trySave results let cleanup todos = let results = todos.Todos |> List.filter (function | TodoTask(_) -> true | _ -> false) |> fun ts -> {todos with Todos = ts} trySave results let clear todos = let results = {todos with Todos = []} trySave results let private print = function | TodoTask {Note = note} -> printf $"\u001b[31m✖\u001b[0m {note}" | DoneTask {Note = note} -> printf $"\u001b[32m✓\u001b[0m {note}" let printAll todos = todos.Todos |> List.iteri (fun i t -> printf $"{i + 1} " print t printfn "") type Args = | [] Note of note: string | [] Ls | [] Remove of index: int | [] Check of index: int | [] Undo of index: int | [] Cleanup | [] Clear interface IArgParserTemplate with member x.Usage = match x with | Note _ -> "Task to do" | Ls -> "List unchecked tasks" | Remove _ -> "Remove a task by index" | Check _ -> "Check a task by index" | Undo _ -> "Undo a task by index" | Cleanup -> "Clear checked tasks" | Clear -> "Clear all tasks" let argParser = ArgumentParser.Create(programName = "todo") let results = argParser.ParseCommandLine(inputs = fsi.CommandLineArgs[1..], ignoreMissing = true) let home = Environment.SpecialFolder.Personal |> Environment.GetFolderPath let todos = TodoList.load (Path.Combine(home, "todo.txt")) match results.GetAllResults() with | Ls :: _ -> TodoList.printAll todos | cmd :: _ -> let action = match cmd with | Remove i -> TodoList.remove todos i | Check i -> TodoList.check todos i | Undo i -> TodoList.undo todos i | Cleanup -> TodoList.cleanup todos | Clear -> TodoList.clear todos | Note note -> TodoList.add todos note | _ -> fun _ -> Error (Failure "Unkown command") match action () with | Ok tds -> TodoList.printAll tds | Error e -> printfn "%A" e | _ -> printf "%s" (argParser.PrintUsage())