Starter project
I remember when I started programming in python. I looked for anything I could write as a CLI or automate in some way, so in the spirit of that, I decided to write a bit about doing the same using haskell libraries.
I replaced monosnap functionality (sharing screenshots) with this script awhile back. It’s generically useful and here is the whole of it if you’re only interested in the source.
Also, there’s a deeper dive into Wreq here if you find this cursory intro lacking
Bam! Some code
The important imports:
import Data.Monoid ((<>)) -- just for glueing together Text's
import Network.Wreq -- the request library
import Control.Lens -- setting and getting params/headers/etc
import Data.Aeson.Lens -- same
Since the main thing this script does is upload a photo anonymously to imgur, we’ll start with that function.
The Request
uploadAndReturnUrl :: IO String
uploadAndReturnUrl = do
imagePath <- parseArgs
cid <- clientId
let authHeader = defaults & header "Authorization" .~ ["Client-ID" <> " " <> cid]
let payload = [ partText "type" "file"
, partFile "image" imagePath
]
res <- postWith authHeader "https://api.imgur.com/3/image.json" payload
let guid = res ^. responseBody . key "data" . key "id" . _String
return $ "http://imgur.com/" ++ T.unpack guid
Let’s talk about what might be confusing here. The declaration of authHeader
; what is that?
-- in this context, simply a merge
(&) :: a -> (a -> b) -> b
-- build the header to combine with the defaults :: Options
header :: HeaderName -> ([ByteString] -> f [ByteString]) -> Options -> Options
If you wanted to, you could think of this as an equivalent to the ruby code:
defaults = {:params => [], :headers => []}
defaults[:headers].merge({ :Authorization => "Client-ID #{cid}" })
Fortunately, our code using Lens’ is much more fail-safe. (you could read more here or one of the plethora of other lens tutorials online)
The actual post is pretty self explanatory if you know what authHeader
and payload
are, but here’s the type sig anyway:
postWith :: Postable a => Options -> String -> a -> IO (Response ByteString)
The Postable
typeclass refers to our [Part]
which we constructed in the payload declaration, nbd.
The Response
res ^. responseBody . key "data" . key "id" . _String
We use ^.
to pull out the image id returned from imgur. Unfortunately, lens’ compose left to right, unlike normal functions in haskell so this is responseBody.data.id
The equivalent ruby code might be something like:
response.body[:data][:id]
Again, ours is a bit safer.
Wrap Up
There’s one other piece which grabs the most recent screenshot.
getRecentPath :: IO FilePath
getRecentPath = do
home <- getHomeDirectory
d <- return (joinPath [home, "Desktop"])
files <- globDir1 (compile "Screen Shot*") d
case headMaybe (reverse (sort files)) of
Nothing -> error "no recent screenshots"
Just a -> return a
This could be improved by returning an Either String FilePath
instead of raising an error, but if we stopped and optimized every chance we had in haskell we’d never finish writing what we started.
At the end of all this, we can screen cap and pop over to a terminal:
imgup --screenshot | pbcopy
Now we have an imgur link in our copy buffer which (for me) completely replaces monosnap’s functionality.