Dyota's blog

PowerShell: How I work (II): time tracking

I manage my tasks at work with three PowerShell modules, all of which I wrote myself.

They are:

  1. A to-do list
  2. A time tracking list
  3. A done list.

All of these modules are independent, and mostly do not talk to each other.

td To-Do

This function appends items to a text file as my to-do list, with a timestamp. This is the whole of my to-do list system. It is nothing more sophisticated than a text file, and I find that this has been the most practical to-do list I've had in my whole career. I don't have to open up a different application and I can note down to-do items as I'm working.

The best thing about this is that it is in the terminal, which I use every day anyway. I used to use dedicated softwares to manage my tasks, including Excel files and web-based task tracking apps. I find that I don't need sophisticated features like tagging, grouping, deadlines, etc. The only thing I need is a timestamp on each to-do item.

Nothing beats typing something in the terminal, without breaking the flow of what I am working on at the moment, to record a to-do for later.

I add a new item by typing this into my terminal:

td 'This is a new to-do item'

I view all open to-dos by typing:

td

I sort my tasks manually. I open it up in a text editor (normally in Vim) and move things up an down, put them under categories, and so on. I have a timestamp on every item so I know how many days ago it was put in.

Once I finish a task, I delete it. I don't need to know when I finished it, or keep a historical record. Once in a while, I open up the text file and I delete the items that are done. I open up the file with:

td -edit
Code

function td {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$thing,
        [Parameter()]
        [switch]$edit,
        [Parameter()]
        [switch]$print
    )
    <# 
        Global variables and functions
        $td is the path of the text file
        nvim calls NeoVim
        #>
# This is the edit branch, to edit the text file in a text editor (mine is NeoVim)
if ($edit) {
    # Change all line endings to LF only instead of CRLF
    ((Get-Content $td) -replace "`r`n", "`n") | Set-Content $td

    # I use NeoVim as my text editor
    nvim $td;

    # Clear the console
    Clear-Host;
    
    # Display all to-do items
    td;

    break;
}
else {
    # If there is nothing in the argument, then display the whole to-do list
    if ($thing.length -eq 0) {
        Clear-Host;
        Get-Content $td |
            % {
                $parts = $_.split('#')
                if ($parts.Count -gt 1) {
                    $task = $parts[1]

                    # From the timestamp, determine whether it was today, yesterday, or X days ago
                    $daysAgo = ((get-date) - (get-date $parts[0])).days

                    switch ($daysAgo) {
                        0 { return "today $task"}
                        1 { return "yesterday $task"}
                        default { return "$($daysAgo.ToString().PadLeft(3)) days ago $task" }
                    }
                } else {
                    # If there is no timestamp, then just display the to-do item
                    $_
                }
            }
        
        # Display each item in the console
        Write-Host "($((Get-Content $td | ? { $_ -like '*[ ]*' }).length) items in todo list)`n"
    }
    else {
        # This is for adding a new item to the end of the list
        # Get the timestamp right now
        $date = (Get-Date).ToString("dd-MM-yy")

        # Write the new line into the list
        "$date#[ ] $thing"  |
            Add-Content -Path $td -Encoding "UTF8"
    }
}

# Display the text file in a browser for printing
if ($print) {
    s 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge' "file:///$td"
}

}

tt Time tracking

This was inspired by a python module of the same name.

When I start a task, I mark that I have started it, and my function logs it in a .json file. While the task is in progress, it will show the name of the task in my terminal prompt, so that whenever I look at my terminal (which is often) I can see what I am supposed to be working on.

This guards against forgetting what my current task is, which can sometimes be thrown off by impromptu phone calls, going down rabbit holes, or other distractions (of my own doing).

To start a new task:

tt-start -name 'do this thing' -project 'big project' -role 'analyst'

To end a project:

tt-end 'this is an ending comment'
# the string argument here gets written into done.txt
Code

using namespace System.Collections # for List

<# Global variables and functions $documents is my home directory $ttfile is the path to the JSON file #>

function tt-get { return (Get-Content $ttfile | ConvertFrom-Json) }

function tt-start { param ( # Specifies a path to one or more locations. Wildcards are permitted. [Parameter(Mandatory)] [string] $name,

    [Parameter(Mandatory)]
    [string] $project,

    [Parameter(Mandatory)]
    [string] $role
)
[ArrayList] $tt = tt-get
$lasttask = $tt[-1]

# if there is a task already in progress, disallow making a new task

if ($null -eq $lasttask.end) {
    Write-Host  You are still working on `"$($lasttask.name)`". Finish this one before starting another task.
    break
}

} else { # if the new task doesn't have a name, prompt for a name and break if ((nulleqname -or ($name.length -eq 0))) { Write-Host "You need a name for your new task" break } $newtask = [ordered] @{ "role" = $role; "project" = $project; "name" = $name; "start" = (Get-Date -Format o) } }

[void] $tt.Add($newtask)
$json = ($tt | ConvertTo-Json)
$json > $ttfile

}

function tt { $tt = tt-get lasttask=tt[-1]

if ($null -ne $lasttask.end) {
    Write-Host "Not working on anything. Last worked on " -NoNewLine
    Write-Host -ForegroundColor Red "$($lasttask.name) " -NoNew
    Write-Host -ForegroundColor Red "($($lasttask.project))" -NoNew
    Write-Host ", finished at " -NoNew
    Write-Host -ForegroundColor Red $((Get-Date $lasttask.end).ToString("hh:mm")) -NoNew
    Write-Host "." -NoNew
}
else {
    Write-Host "Currently working on " -NoNewLine
    Write-Host -ForegroundColor Yellow $($lasttask.name) -NoNewLine
    Write-Host " since " -NoNewLine
    Write-Host -ForegroundColor Yellow $((Get-Date $lasttask.start).ToString("hh:mm")) -NoNewLine
    Write-Host "." -NoNewLine
}

}

function tt-end ([string] $note) { $tt = tt-get

# If the last entry already has a end time, then must be not working on anything at the moment.
if ($null -ne $tt[-1].end) {
    Write-Host  'Not working on anything at the moment';
    break
}

# if "end" is not an existing property of this last object, then make one
if (-not ('end' -in ($tt[-1] | Get-Member | ForEach-Object Name)) ) {
    $tt[-1] | Add-Member -NotePropertyName 'end' -NotePropertyValue (Get-Date -Format o)
}
else {
    $tt[-1].end = (Get-Date -Format o)
}

# automatically write into "done.txt"
done $note

$tt | ConvertTo-Json > $ttfile

}

function tt-note ([string] $note) { [ArrayList] $tt = tt-get $lasttask # check if there is a note property }

function tt-edit { nvim $ttfile }

function tt-summary { tt-get | group {(get-date $.start).date} | % { Write-Host (get-date $.Name).ToLongDateString() -ForegroundColor Green

    $_.group |
        group project |
        % {
            $hours = 0
            $_.group |
                % {
                    $duration = if ($null -ne $_.end) {
                        $_.end - $_.start
                    }
                    $hours += $duration.TotalHours
                }
            $t = $([DateTime]::Today.AddHours($hours))

            Write-Host $_.name
            Write-Host `t $t.Hour hours, $t.Minute minutes
        }
    
}

}

doneDaily log

I have the reverse of a to-do list, or a "done list". I find that this is a very useful tool, and it does not have to be related to my to-do list at all. I can look back over the day and see what I have actually done.

This is another text file, with timestamped line items. It keeps me accountable to make sure that I keep doing things. It is also useful to be able to look back and check if I have already done something or not.

To add a "done" item:

done 'the big task of the day'

Sometimes I like to print out this list and look over the week gone by. I find that looking back gives a bit of perspective on what's been done, and indicates what needs to be done next.

The -print argument turns the list into a HTML file, and throws open a browser tab to open it in.

# get the done items from the last 5 days and show it as HTML, for printing
done -last 5 -print
Code

function done ( $item, $last, $search, [switch] $all, [switch] $edit, [switch] $print){
    <# 
        Global variables and functions
        $done is the text file
        nvim is NeoVim
    #>
if ($edit) {
    # Change all line endings to LF only instead of CRLF
    ((Get-Content $done) -replace "`r`n", "`n") | Set-Content $done
    
    # Open up the file in my text editor, NeoVim
    nvim $done;
    
    # Clear the console
    Clear-Host;
    
    # Display only today's items
    done -last 1;
    break;
}
else {
    if ($item.length -eq 0) {
        
        # If there is no item to be put in, then display done items
        
        # Set up an array for displaying the data as a table of PSCustomObjects
        [System.Collections.ArrayList] $rows = @();
        
        
        $content = if ($null -eq $search) {
            
            # If there no search term argument, then get everything
            (Get-Content $done)

        } else {

            # If there is a search term argument, then select only the lines that match
            (Get-Content $done) | Select-String $search

        }
        
        $content |
            ForEach-Object {
                # Use regex to pull out date, time, and task from the line of text
                $_ -match "(?<date>\d{4}-\d{2}-\d{2}) (?<time>\d{2}:\d{2}) (?<task>(?<=\d{2}:\d{2} ).+$)" | Out-Null

                # Put in the item as a hashtable
                $rows.Add(@{
                    date = (Get-Date $matches.date -Format "ddddd, dd MMM \'yy")
                    realDate = (Get-Date $matches.date -Format o)
                    time = $matches.time
                    task = $matches.task
                }) | Out-Null;
            }

        # This is a table of PSCustomObjects
        $table = $rows | ConvertTo-Json | ConvertFrom-Json
        
        # $last means "the last X days"
        # if no -last was specified, then set $last to 1 (default only to showing today)
        if ($null -eq $last) {
            $last = 1
        }

        # Get the last X dates if there is a -last argument
        $selectedDates = $table.date | distinct | Select-Object -Last $last

        # Filter the table
        $filtered = $table |
            Where-Object {
                if ($all) {
                    $true
                } elseif ($null -ne $last) {
                    return $_.date -in $selectedDates
                } else { <# nothing#> }
            }

        
        if ($print) {
            # if -print is specified, then set up a HTML table ready for printing, and display in the browser
            $sb = [StringBuilder]::new()
            
            [void] $sb.AppendLine("<head><style>td {vertical-align: top;} </style></head>")
            # do a printable HTML table
            $filtered |
                Group-Object -Property realDate |
                ForEach-Object {
                    [void] $sb.AppendLine("<h3>$(Get-Date $_.Name -Format "ddddd, dd MMM \'yy")</h3>")
                    [void] $sb.AppendLine("<table>")
                    $_.Group |
                        ForEach-Object {
                            [void] $sb.AppendLine("<tr><td>$($_.time)</td><td>$($_.task)</td></tr>")
                        }
                    [void] $sb.AppendLine("</table>")
                }

            $sb.ToString() > "$documents\done.html"

            Start-Process "$documents\done.html"
        } else {
            
            # if -print was not specified, then display in the terminal
            $filtered | Format-Table -GroupBy date -Property time, task
        }
    }
    else {
        
        # If there was an argument put in, then append this to the text file
        
        # Get a timestamp
        $date = (Get-Date -Format "yyyy-MM-dd HH:mm");
        
        # Append to file
        "$date $item" |
            Add-Content -Path $done -Encoding "UTF8"
    }
}

}

#powershell #taskmanagement #timetracking #work