Giving Go another chance: display command

So far I got only to C in my CRUD application. It’s time to move on to R. I wanted to display a list of entries in the log similar to what timetrap does:

Id  Day                Start      End        Duration   Notes
40  Sun Jan 20, 2019   22:00:00 - 01:00:00   3:00:00    initial setup
                                             3:00:00
41  Mon Jan 21, 2019   22:37:23 - 00:37:26   2:00:03    cli and cobra
                                             2:00:03
43  Tue Jan 22, 2019   11:40:14 - 12:01:56   0:21:42    tags
44                     12:02:08 - 12:34:00   0:31:52    parse tags from the comment
                                             0:53:34
48  Wed Jan 23, 2019   00:57:40 - 01:34:35   0:36:55    out command
49                     01:34:53 - 02:06:28   0:31:35    fix tag parsing
                                             1:08:30
59  Sat Jan 26, 2019   21:52:17 - 22:42:48   0:50:31    sqlite
60                     23:37:25 - 00:11:05   0:33:40    sqlite
                                             1:24:11
61  Sun Jan 27, 2019   15:12:51 - 15:29:04   0:16:13    sqlite
                                             0:16:13
62  Mon Jan 28, 2019   00:13:13 - 00:47:54   0:34:41    sqlite
63                     22:30:26 - 22:53:47   0:23:21    backend
64                     23:24:25 - 00:37:21   1:12:56    backend
                                             2:10:58
66  Tue Jan 29, 2019   11:19:19 - 11:20:36   0:01:17    display
67                     11:24:10 - 11:42:07   0:17:57    display
68                     11:51:51 - 11:52:53   0:01:02    display
69                     12:57:04 - 13:01:14   0:04:10    display
70                     13:10:37 - 14:03:54   0:53:17    display
71                     15:00:21 - 15:31:03   0:30:42    display
72                     21:11:52 - 21:16:23   0:04:31    display
73                     21:16:29 - 23:18:54   2:02:25    display
                                             3:55:21
74  Wed Jan 30, 2019   18:38:29 - 18:43:57   0:05:28    ids
75                     20:27:47 - 21:42:52   1:15:05    ids
                                             1:20:33
    -------------------------------------------------------
    Total                                   16:09:23

This, by the way, is how much time I’ve dedicated so far to this project. And all those tiny little bits of time I get to work on this between dealing with small kids, changing diapers, feeding them and putting out other fires at home. Talking about programmer productivity and interruptions.

This feature turned from a quick one into a time sucking endeavor. First the Backend interface had to be changed to support retrieval of completed and open entries.

// Backend ...
type Backend interface {
    OpenEntry(comment string, startTime time.Time, tags []string) error
    CloseEntry(endTime time.Time) error

    GetEntries() ([]Entry, error)
    GetOpenEntry() (Entry, error)
}

Oh, the error handling! Why not just have error a part of every function in Go? I’ll talk about this in a separate post. So far I could say I’m happy with the simplistic nature of Go. The only thing I’m suffering from so far is the error handling. if err != nil { return err }.

My database format now spans three different files. entries.json to store the entries themselves. current.json to store the currently open entry, which is later moved to entries.json once it’s closed. And id.json which stores the last used index. I’m starting to regret the decision not to use SQLite, which would provide me with all that plumbing and free up some time to learn and google SQL queries. For now I’m also ignoring the problem of preventing two instances of the program trying to modify the database at the same time and turning it all into smoking rubble.

Briefly I tried to return a channel instead of an array of entries. I tried to be smart and find some use of Go channels in my program. Though it was a breeze to write, it was not really fitting into my usage patterns. So I decided to use simple arrays and worry about looking pro and hip later.

To display the entries I shopped around for a library again. And this, I think, is a really strong point of using Go. There’s a library for anything. Even though C++ has been around for a lot longer, it’s often impossible or really difficult to find libraries that do half of what Go libraries do. Writing a tool like this in C++ would be difficult. Anyhow, the library I found is called tablewriter. It’s pretty flexible and draws nice looking tables.

$ klk display --grep 'code|test'
+----+-------------+----------+----------+-----------------------------+-------------+
| ID |     DAY     |   TIME   | DURATION |           COMMENT           |    TAGS     |
+----+-------------+----------+----------+-----------------------------+-------------+
|  2 | 30 Jan 2019 | 21:34:17 |    15:14 | Writing tests               | #test       |
|  3 |             | 21:55:04 |    20:08 | Writing code                | #code       |
|  4 |             | 22:20:38 |    25:05 | Writing more code and tests | #code #test |
+----+-------------+----------+----------+-----------------------------+-------------+
|                     TOTAL   | 1:00:27  |
+----+-------------+----------+----------+-----------------------------+-------------+

I wasted a huge amount of time trying to make the report look nicer. In the process I figured out I’m not a good UI designer, even if it’s just a basic text UI. I think I’ll just copy what timetrap prints. For now this will do.

Now my application is almost 500 lines of code. That doesn’t sound much, but it could already do the most basic time tracking. I’d say 90% of what I use in timetrap. Next step would be to worry about U and D. And more sophisticated tag filtering, the whole reason I started this.

One thing that worries me a little is the executable size. I’m at 11 megabytes now and the app doesn’t do very much. When I tried SQLite, it was 16 MB. Sounds a bit excessive. I’ll investigate later, but I think the most of the bulk is coming from Cobra. I might consider getting rid of, since I don’t use any of the advanced features anyway.


Google searches that went into getting this to work:

  • golang channel
  • golang make empty channel
  • golang close channel
  • golang read from channel
  • golang for range
  • golang for range channel with index
  • golang display table
  • golang read line
  • golang unmarshal string
  • golang tostring
  • golang create empty slice
  • golang regexp flags
  • golang int to string
  • golang golang cannot define new methods on non-local type
  • golang format duration
  • golang init primitive type
  • golang read text file as string
  • golang sizeof
  • golang sizeof int
  • golang log fatal
  • golang exit vs panic
  • golang recover
  • golang fallthrough case
  • golang atexit

  • Time spent: 5:15 hours
  • Total time spent: 16:15 hours