Logo

dev-resources.site

for different kinds of informations.

Hacking Watson with Haskell - Part 1

Published at
8/15/2024
Categories
haskell
lifehack
Author
vst
Categories
2 categories in total
haskell
open
lifehack
open
Author
3 person written this
vst
open
Hacking Watson with Haskell - Part 1

Watson is a command-line tool that helps you to track your time. It is simple and powerful, yet it lacks some features that I would like to have. In this blog post, I will start hacking Watson with Haskell.

Motivation

I have been using Watson since end of 2022. It is a simple and powerful tool that taught me how I spend my time.

On one hand, it lacks some features such as taking notes, integrating with my calendar or TODO list, etc... On the other hand, it keeps the data in a very simple format that can be easily read and manipulated.

I have been thinking about hacking Watson for a while. I have some ideas such as bulk editing, integrating with my pomodoro timer and tracking if my daily plan and its execution are aligned.

I am planning to do a few blog posts both to document my journey and to share my progress.

Program

This blog post is a Literate Haskell program that reads the Watson frames from a JSON file and prints them to the standard output. That's it for this blog post.

Before we start, how does a Watson frames file look like?

A Watson frames file is a JSON array of Watson frames. And a Watson frame is a JSON array with 6 elements:

[
  1629043200, // Start time
  1629046800, // End time
  "project", // Project name
  "frame-id", // Unique frame ID
  ["tag1", "tag2"], // Tags
  1629046800 // Update time
]
Enter fullscreen mode Exit fullscreen mode

Let's start with the language extensions:

{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
Enter fullscreen mode Exit fullscreen mode

We will use one Haskell package in addition to the libraries coming with the GHC: aeson. Here are our imports:

import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import System.Environment (getArgs)
import qualified Data.Aeson as Aeson
import qualified Data.Text as T
import qualified Data.Time as Time
Enter fullscreen mode Exit fullscreen mode

Our entry point function has 3 jobs to do:

  1. Read the path to the frames JSON file from the command line arguments.
  2. Attempt to read the frames from the file.
  3. Print the frames to the standard output if reading is successful, otherwise print an error message.
main :: IO ()
main = do
Enter fullscreen mode Exit fullscreen mode

First, attempt to read the path from the command line arguments. Note that the head function is a partial function and it will throw an exception if the list is empty. We are using it here because we are expecting the path to the frames JSON file as the first argument. And I am lazy:

  fp <- head <$> getArgs
Enter fullscreen mode Exit fullscreen mode

Then, attempt to read the frames from the file. We are using the readFrames function that we will define later. It returns an Either value:

  eFrames <- readFrames fp
Enter fullscreen mode Exit fullscreen mode

By now, we have a result of type Either String [Frame]. We will pattern match to print the frames to the standard output if reading is successful, otherwise print an error message:

  case eFrames of
    Left err -> putStrLn ("ERROR! " <> err)
    Right frames -> mapM_ print frames
Enter fullscreen mode Exit fullscreen mode

That's as far as our main function goes. Now, let's define the Frame data type which is a simple record type:

data Frame = Frame
  { frameId :: !T.Text
  , frameSince :: !Time.UTCTime
  , frameUntil :: !Time.UTCTime
  , frameProject :: !T.Text
  , frameTags :: ![T.Text]
  , frameUpdatedAt :: !Time.UTCTime
  }
Enter fullscreen mode Exit fullscreen mode

... and add the Show and Eq instances for it:

  deriving (Show, Eq)
Enter fullscreen mode Exit fullscreen mode

Our program still does not know how to read the Frame data type from a JSON file. We will define an instance of the FromJSON type class for it:

instance Aeson.FromJSON Frame where
  parseJSON v = do
Enter fullscreen mode Exit fullscreen mode

Here, we will ask the parser to parse the JSON value into an array which is inferred from the pattern match:

    arr <- Aeson.parseJSON v
Enter fullscreen mode Exit fullscreen mode

Let's pattern match. We need an array with exactly 6 elements:

    case arr of
      [fSince, fUntil, fProj, fId, fTags, fUpdated] -> do
Enter fullscreen mode Exit fullscreen mode

Now we can continue parsing individual elements. Note that we are using exactly the same names for the fields as in the Frame record so that we can use RecordWildCards extension to fill the record at the end:

        frameId <- Aeson.parseJSON fId
        frameSince <- fromEpoch <$> Aeson.parseJSON fSince
        frameUntil <- fromEpoch <$> Aeson.parseJSON fUntil
        frameProject <- Aeson.parseJSON fProj
        frameTags <- Aeson.parseJSON fTags
        frameUpdatedAt <- fromEpoch <$> Aeson.parseJSON fUpdated
        pure $ Frame {..}
Enter fullscreen mode Exit fullscreen mode

If there no less or more than 6 elements in the array, we should fail with an error message:

      _ -> fail "Frame: expected an array of 6 elements"
Enter fullscreen mode Exit fullscreen mode

Finally, we will define the fromEpoch function that converts an epoch time to a UTCTime value:

    where
      fromEpoch = posixSecondsToUTCTime . fromIntegral @Int
Enter fullscreen mode Exit fullscreen mode

Now, we can define the readFrames function that reads the frames from a JSON file:

readFrames :: FilePath -> IO (Either String [Frame])
readFrames fp = do
  frames <- Aeson.eitherDecodeFileStrict fp
  pure $ case frames of
    Left err -> Left ("Failed to parse frames: " <> err)
    Right fs -> Right fs
Enter fullscreen mode Exit fullscreen mode

Done!

As usual, let's create a symbolic link to the source code file:

ln -sr \
  content/posts/2024-08-15_hacking-watson-part-1.md  \
  content/posts/2024-08-15_hacking-watson-part-1.lhs
Enter fullscreen mode Exit fullscreen mode

Let's download sample Watson frames JSON file:

curl -sLD - -o /tmp/frames.json https://raw.githubusercontent.com/TailorDev/Watson/master/tests/resources/autocompletion/frames
Enter fullscreen mode Exit fullscreen mode

..., run our program:

runhaskell -pgmLmarkdown-unlit content/posts/2024-08-15_hacking-watson-part-1.lhs /tmp/frames.json
Enter fullscreen mode Exit fullscreen mode

..., and see the output:

Frame {frameId = "41dcffb7bd74442794b9385c3e8891fc", frameSince = 2018-10-16 09:46:15 UTC, frameUntil = 2018-10-16 10:27:29 UTC, frameProject = "project1", frameTags = ["tag1"], frameUpdatedAt = 2018-10-16 10:27:29 UTC}
Frame {frameId = "e8b53f1dda684672806e0f347d2b11fc", frameSince = 2019-01-04 07:56:31 UTC, frameUntil = 2019-01-04 08:10:27 UTC, frameProject = "project2", frameTags = ["tag2"], frameUpdatedAt = 2019-01-04 08:10:27 UTC}
Frame {frameId = "ef9933131f254b6fa94dda2a85107195", frameSince = 2019-02-13 13:39:59 UTC, frameUntil = 2019-02-13 14:30:00 UTC, frameProject = "project1", frameTags = ["tag1"], frameUpdatedAt = 2019-02-13 14:51:33 UTC}
Frame {frameId = "f4f78aa79744440b9cbd28edef1ba0b0", frameSince = 2019-02-14 13:20:00 UTC, frameUntil = 2019-02-14 13:48:37 UTC, frameProject = "project3-A", frameTags = ["tag2"], frameUpdatedAt = 2019-02-14 13:48:37 UTC}
Frame {frameId = "10c6ff8612c84b239b5141cd04f10415", frameSince = 2019-02-22 10:00:00 UTC, frameUntil = 2019-02-22 10:34:02 UTC, frameProject = "project3-A", frameTags = ["tag2"], frameUpdatedAt = 2019-02-22 10:34:02 UTC}
Frame {frameId = "e113e26dbf8d4db3ba6361a73709ffd6", frameSince = 2019-03-07 10:20:00 UTC, frameUntil = 2019-03-07 11:06:08 UTC, frameProject = "project1", frameTags = ["tag2"], frameUpdatedAt = 2019-03-07 11:06:08 UTC}
Frame {frameId = "d5185c8e811a40efbad43d2ff775f5e8", frameSince = 2019-03-13 15:12:46 UTC, frameUntil = 2019-03-13 15:50:00 UTC, frameProject = "project2", frameTags = ["tag2"], frameUpdatedAt = 2019-03-14 07:50:48 UTC}
Frame {frameId = "379f567a9d584498aa8729a170b8b8ad", frameSince = 2019-03-25 09:45:14 UTC, frameUntil = 2019-03-25 10:28:55 UTC, frameProject = "project3-B", frameTags = ["tag2"], frameUpdatedAt = 2019-03-25 10:28:55 UTC}
Frame {frameId = "f4f7429d70454175bb87ce2254bbd925", frameSince = 2019-04-12 06:30:00 UTC, frameUntil = 2019-04-12 07:00:00 UTC, frameProject = "project4", frameTags = ["tag3"], frameUpdatedAt = 2019-04-12 10:00:10 UTC}
Frame {frameId = "af9fe637030a465ba279abc3c1441b66", frameSince = 2019-04-30 09:09:29 UTC, frameUntil = 2019-04-30 09:30:25 UTC, frameProject = "project3-B", frameTags = ["tag3"], frameUpdatedAt = 2019-04-30 09:30:26 UTC}
Enter fullscreen mode Exit fullscreen mode

Wrap-Up

This was an exciting blog post for me, because it is a new journey to new absurds.

I needed to read the Watson frames from a JSON file, so that I can start manipulating them. We now have an internal data definition for the Watson frames and a program that reads them from a JSON file. We are ready for the next step...

lifehack Article's
30 articles in total
Favicon
â›”Stop Excessive Validation, you are ruining my life hack!
Favicon
Completed My Blogging Challenge
Favicon
Hacking Watson with Haskell - Part 3
Favicon
Hacking Watson with Haskell - Part 2
Favicon
Hacking Watson with Haskell - Part 1
Favicon
Don’t Panic! The Hitchhiker’s Brew to Serenity
Favicon
Why Multi-tasking is not Good (Bite-size Article)
Favicon
Using Task Management Tools to Forget Tasks (Bite-size Article)
Favicon
Life Hacks: Make the Most of Your Commute (Bite-size Article)
Favicon
On personal goal-setting
Favicon
Power of Logseq - Manage Your Daily Tasks with Outliner
Favicon
How To Customize Your LinkedIn URL in 6 Easy Steps AKA How To Boost Your LinkedIn SEO AKA Lifehacks By Miki Szeles
Favicon
making tough decisions fast
Favicon
Transform any Telegram bot into (almost) an app
Favicon
Why isn't coverage badge in GitLab updating?
Favicon
Code Reactjs Faster by Enabling Emmet for JavaScript and TypeScript in VS Code
Favicon
Automated Form Filling in 4 Steps | No Coding Skills Required
Favicon
A Python package that makes it easier to work with lists on Twitter
Favicon
Git Conflict resolution made simple
Favicon
Finding new music on Spotify by aggregating the choices of tastemakers
Favicon
Transfer files between your phone and computer without any cable and internet!
Favicon
100% Reliability Myth
Favicon
A tip to declutter unused cables with a tape writer and pouches
Favicon
How to get things done in real. A step by step guide.
Favicon
Optimize Your Notetaking With Glyphs
Favicon
Dealing with programmer's burnout
Favicon
Be a good host and share your WiFi
Favicon
Squashing Software Defects with Eisenhower Matrix
Favicon
🗣 Communication Hack – Use Labelled Links
Favicon
Tired of turning Wifi/Bluetooth/Data off in Settings? (iPhone)

Featured ones: