Skip to content

Commit

Permalink
Initial public commit
Browse files Browse the repository at this point in the history
  • Loading branch information
marcogario committed May 15, 2022
1 parent 242fd86 commit 59dae1d
Show file tree
Hide file tree
Showing 18 changed files with 913 additions and 2 deletions.
12 changes: 12 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"fantomas-tool": {
"version": "4.7.9",
"commands": [
"fantomas"
]
}
}
}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**/bin/**
**/obj/**
.fake
.ionide
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"FSharp.inlayHints.parameterNames": false,
"FSharp.inlayHints.typeAnnotations": false
}
86 changes: 84 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,84 @@
# time-track
A CLI time tracking tool
# Time Tracking (`tt`)

The main operation of our Time Tracker is to keep track of *Activities*. An Activity represents a continuous stretch of time that we spent doing something.
When we add an Activity to the Time Tracker we say that we *log* (verb) the Activity. We then refer to the *Log* (noun) as the set of Activities we have logged.

We want to mine the Log to extract insights on how we spend our time. To do so, we want to generate *Reports*. A Report is a summary of the time we spent on various activities during a *Period* (e.g., day, week, work-week). To make our Reports more useful, we use *Keywords* to annotate activities and leverage Keywords to filter and aggregate Activities.[^1]

[^1]: Reporting is limited to total tracked time in the current week for release 1.

### CLI Example

To input activities in the Time Tracking, you can specify the start and end of an activity.
The activity is made up of keywords that will be used for reporting and filtering.

```
$ tt start yoga
Starting yoga at 9:00
$ tt end
Ending yoga at 9:20 (elapsed 00:20)
$ tt start work emails
Starting work emails at 9:30
$ tt end
Ending work emails at 10:00 (elapsed 00:30)
```

You can also omit the start of the activity for contiguous activities:

```
$ tt start coffee-break
Ending work emails at 10:05 (elapsed 00:05)
Starting coffee-break
```

To display all entries for the day we use `show`:

```
$ tt show
09:00 - 09:20 : Yoga
09:30 - 10:00 : Work Emails
10:00 - 10:05 : Coffee Break
10:05 - 11:55 : Pair with John Issue-37
11:55 - 12:10 : Pairing follow-up
12:15 - 12:35 : Work Emails
12:35 - >>>>> : Lunch
Total: 03:20
```

The last entry is on-going, so the end-time is not included.

At the end of the week (day or month), we can extract a report. In the current version the report only shows the total for the current week (Mon-Sun):

```
$ tt report
Weekly Total: 00:35
```

Logs are stored under `~/.tt/` (configurable via `TIMETRACKER_DIR` env) one file per day using the following CSV structure:
```
start, end, description
```

where:
* `start` and `end` are timestamps in ISO-8601 format
* `end` might be empty
* `description` is a quoted string

Our examples would look like (omitting milliseconds and tzinfo):

```csv
start , end , description
2022-01-22T09:00:00,2022-01-22T09:20:00,"yoga"
2022-01-22T09:30:00,2022-01-22T10:00:00,"work emails"
2022-01-22T10:00:00,2022-01-22T10:05:00,"coffee break"
2022-01-22T10:05:00,2022-01-22T11:55:00,"pair with John Issue-37"
2022-01-22T11:55:00,2022-01-22T12:10:00,"pairing follow-up"
2022-01-22T12:15:00,2022-01-22T12:35:00,"work emails"
2022-01-22T12:35:00,,"lunch"
```

NOTE: There are a few ambiguities around CSV and delimiters that tend to be implementation dependent (how to represent line breaks, quotes etc.) As our descriptions are meant to be rather simple, we are not going to worry about them for now.

105 changes: 105 additions & 0 deletions TimeTracker.Cli/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
open System
open System.IO
open TimeTracker.Storage
open TimeTracker.Core

module Cli =

let DEFAULT_STORE_DIR =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), ".tt")

let STORE_DIR =
match Environment.GetEnvironmentVariable("TIMETRACKER_DIR") with
| null -> DEFAULT_STORE_DIR
| v -> v

let logPath (date: DateTime) =
let dateStr = date.ToString "yyyyMMdd"
Path.Combine(STORE_DIR, dateStr + ".csv")

let todayLog = logPath DateTime.Today

// Returns the current time truncated to the minute
let timeNow =
let truncate = fun (d: DateTime) -> d.AddTicks(-(d.Ticks % TimeSpan.TicksPerMinute))
truncate DateTime.Now

let endCurrentActivityIfAny log : ClosedLog =
log
|> Log.fold
(fun l ->
let newLog = l |> Log.endCurrentActivity timeNow
let a = Log.Last(Closed newLog)
printf "End: %s" a.Description
printf " (elapsed %s)\n" ((Activity.Duration a).ToString("hh\\:mm"))
newLog)
id

let start desc log =
Active(
log
|> endCurrentActivityIfAny
|> Log.startActivity
{ Start = timeNow
Description = desc
End = None }
|> fun log ->
printf "Start: %s\n" desc
log
)

let editLogErrorToString (msg, e) = msg + "\n" + e.ToString()

let withTodaysLogDo f =
f
|> editLog todayLog
|> Result.mapError editLogErrorToString

let startCmd desc =
withTodaysLogDo (fun log -> Some(start desc log))

let endCmd () =
withTodaysLogDo (fun log -> Some(Closed(endCurrentActivityIfAny log)))

let showCmd () =
withTodaysLogDo (fun log ->
printf "%s\n" (Log.ToString log)
printf "Total: %s\n" ((Log.TotalTime log).ToString("hh\\:mm"))
None)

let reportCmd () =
let printTotal logs =
let report = Report.weekReport timeNow logs
let total = report.TotalTime
printf "Weekly Total: %s\n" (total.ToString("hh\\:mm"))

match loadAllLogs STORE_DIR with
| Ok logs ->
Ok(
printTotal logs
Log.Empty
)
| Error e -> Error e



let errorCheck =
function
| Ok _ -> 0
| Error e ->
printfn "Error: %s" (e)
-1

[<EntryPoint>]
let main args =
if args.Length > 0 then
match args.[0].ToLower() with
// TODO: The return type of the Cmd should be Result<unit, string>
| "start" -> Cli.startCmd (args |> Seq.skip 1 |> String.concat " ")
| "end" -> Cli.endCmd ()
| "show" -> Cli.showCmd ()
| "report" -> Cli.reportCmd ()
| _ -> Error "Must provide one of start|end|show|report"
else
Cli.showCmd ()
|> errorCheck
16 changes: 16 additions & 0 deletions TimeTracker.Cli/TimeTracker.Cli.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\TimeTracker\TimeTracker.fsproj" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions TimeTracker.Tests/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module Program = let [<EntryPoint>] main _ = 0
Loading

0 comments on commit 59dae1d

Please sign in to comment.