The project we will highlight today is pretty straightfoward: a calendar application written in F# for your terminal.
Code overview
Let's analyse some of the F# code
open System
open Argu
module Program =
[<EntryPoint>]
let main args =
let errorHandler =
ProcessExiter(
colorizer =
function
| ErrorCode.HelpText -> None
| _ -> Some ConsoleColor.Red
)
let parser =
ArgumentParser.Create<Arguments>(programName = "my-calendar", errorHandler = errorHandler)
let results = parser.ParseCommandLine args
let args = results.GetAllResults()
MainHandler.handle args
0
We can see that we are making use of a parser to handle the commands given and because this is the main entry code we need an exit code. The number "0" indicates a successful process.
Let's look into the handler:
open System
open Spectre.Console
open FsSpectre
[<RequireQualifiedAccess>]
module MainHandler =
let handle (args: Arguments list) =
let now = DateTime.Now
let data = Storage.retrieve ()
match args with
| [ Show ] -> Views.mainView now data |> AnsiConsole.Write
| [ ToDo subArgs ] ->
let subArgs = subArgs.GetAllResults()
ToDoHandler.handle now data subArgs
| [ Event subArgs ] ->
let subArgs = subArgs.GetAllResults()
EventHandler.handle now data subArgs
| [ Recurring_Event subArgs ] ->
let subArgs = subArgs.GetAllResults()
RecurringEventHandler.handle now data subArgs
| _ -> markup { text "Wrong arguments provided!\nTry `my-calendar --help` for more information.\n" } |> AnsiConsole.Write
A straight-forward process that checks the list of arguments given by the user, a unique thing is the markup function coming from FsSpectre that allows us to output a more rich text output in the console.
Coontinuing on that piece of code, let's see what happens when we want to handle a Todo:
open System
open Spectre.Console
open FsSpectre
[<RequireQualifiedAccess>]
module ToDoHandler =
let handle (now: DateTime) (data: MyCalendarData) (args: ToDoArguments list) =
match args with
| [ ToDoArguments.Add ] ->
let name =
textPrompt<string> { text "What's the name of the new ToDo?" }
|> AnsiConsole.Prompt
let description =
textPrompt<string> { text "Give it a brief description:" } |> AnsiConsole.Prompt
let todo =
{ Id = Guid.NewGuid()
Name = name
Description = description
CreatedAt = now
MarkedDoneAt = None
SoftDeleted = false }
let newToDos = Array.append [| todo |] data.ToDos
let newData = { data with ToDos = newToDos }
Storage.store newData
Views.mainView now newData |> AnsiConsole.Write
| [ ToDoArguments.Edit ] ->
let todo =
selectionPrompt<ToDo> {
title "Select a ToDo you want to edit:"
page_size 10
choices (ToDo.active data.ToDos)
}
|> AnsiConsole.Prompt
let name =
textPrompt<string> {
text "What's the new name of the ToDo?"
default_value todo.Name
}
|> AnsiConsole.Prompt
let description =
textPrompt<string> {
text "What's the new description of the ToDo?"
default_value todo.Description
}
|> AnsiConsole.Prompt
let updatedTodo =
{ todo with
Name = name
Description = description }
let newToDos = ToDo.update updatedTodo data.ToDos
let newData = { data with ToDos = newToDos }
Storage.store newData
Views.mainView now newData |> AnsiConsole.Write
| [ ToDoArguments.Done ] ->
let todo =
selectionPrompt<ToDo> {
title "Select a ToDo you want to mark as done:"
page_size 10
choices (ToDo.active data.ToDos)
}
|> AnsiConsole.Prompt
let markedDone = { todo with MarkedDoneAt = Some now }
let newToDos = ToDo.update markedDone data.ToDos
let newData = { data with ToDos = newToDos }
Storage.store newData
Views.mainView now newData |> AnsiConsole.Write
| [ ToDoArguments.Undone ] ->
let todo =
selectionPrompt<ToDo> {
title "Select a ToDo you want to undo:"
page_size 10
choices (ToDo.markedDone data.ToDos)
}
|> AnsiConsole.Prompt
let unmarkedDone = { todo with MarkedDoneAt = None }
let newToDos = ToDo.update unmarkedDone data.ToDos
let newData = { data with ToDos = newToDos }
Storage.store newData
Views.mainView now newData |> AnsiConsole.Write
| [ ToDoArguments.Delete ] ->
let todo =
selectionPrompt<ToDo> {
title "Select a ToDo you want to delete:"
page_size 10
choices (ToDo.deletable data.ToDos)
}
|> AnsiConsole.Prompt
let deleted = { todo with SoftDeleted = true }
let newToDos = ToDo.update deleted data.ToDos
let newData = { data with ToDos = newToDos }
Storage.store newData
Views.mainView now newData |> AnsiConsole.Write
| _ -> markup { text "Wrong sub arguments provided!\nTry `my-calendar todo --help` for more information.\n" } |> AnsiConsole.Write
A similar process, we can notice that when the argument is [ ToDoArguments.Add ]
we are storing a record of type Todo using a store
function found in the Storage
module:
namespace MyCalendar
open System
open System.IO
open FSharp.Json
[<RequireQualifiedAccess>]
module Storage =
let retrieve () =
let path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"my-calendar-store.json")
let data =
if (File.Exists(path)) then
File.ReadAllText(path) |> Json.deserialize<MyCalendarData>
else
MyCalendarData.Default
data
let store (data: MyCalendarData) =
try
let path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"my-calendar-store.json")
let str = Json.serialize data
File.WriteAllText(path, str)
with ex ->
printfn "%A" (ex.ToString())
This store
function stores the data in a .json
file
Usage
First, please ensure you are using a Windows-based terminal, something like Powershell and not something like bash, otherwise you will run into issues.
Install with
dotnet tool install --global MyCalendar --version 0.1.1
See how your calendar currently looks by running:
my-calendar show
Right now it's empty, let's add a todo:
my-calendar add todo
This will prompt some questions about the todo before adding to the calendar
We can add events in a similar manner but we will have to provide more details
Check the calendar now:
my-calendar show
We can see that the event and todo were added
Let's remove a todo:
my-calendar todo delete
And we will be able to select the todo we wish to remove
Conclusion
The "my-calendar" library is a highly effective tool if you frequently operate from the terminal and aim to incorporate a calendar into your routine tasks.