It's 2020 and taking screenshots is still a problem.

Two decades into 21st century and yet there's still problem with taking screenshots. Don't get me wrong, there are awesome screenshot taking apps out there. Except, there's a something wrong with each and every one of them...

Let's begin with Sharex. Great tool, really. It has everything you might need, and even more. It's open source and free, you can upload your screenshots to almost all imaginable destinations. So what's wrong? It's Windows only. As a person who uses Windows, Mac and Linux, it's a no-go for me.

The next one in line is ScreenCloud. I immediately fell in love with it. It's simple and lightweight, it's got everything I need. I initially tested it on Windows, and it worked great. I went to work the following day and decided to also give it a try on my iMac. First attempt - my screen goes black. Turns out there is a bug and ScreenCloud opens a new virtual desktop in order to take a screenshot. This new virtual desktop then remains open and you can only see darkness inside it. Somehow the app managed to break macOS. On Linux the story is short - sFTP uploads don't work. I guess I can't complain, because the tool is free and I appreciate all the hard work it's creator has put in.

I was still at work and decided to scan through App Store to see if there are any Mac specific applications that would get the job done. I tried a few but in all of the sFTP uploads were a paid feature.

Now, I'm not asking for much. Basically, I want to draw a rectangle on the screen and then upload resulting image to my server. I figured I can do the upload part myself, because screenshot taking mechanism is built into all systems. So I started a new Go project and got down to business.

First I needed an sFTP client. Here's how to create one.

func newSFTPClient() (*sftp.Client, error) {
    // read private key from file
    key, err := ioutil.ReadFile(sshKeyPath)
    if err != nil {
        return nil, err
    }
    // parse private key 
    signer, err := ssh.ParsePrivateKey(key)
    if err != nil {
        return nil, err
    }
    // configure ssh client
    config := &ssh.ClientConfig{
        User: remoteUser,
        // authenticate using provided key
        Auth: []ssh.AuthMethod{
            ssh.PublicKeys(signer),
        },
        // ignore checking host key
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }
    client, err := ssh.Dial("tcp", remoteHost, config)
    if err != nil {
        return nil, err
    }
    return sftp.NewClient(client)
}

I'll skip the part where I look for files in a given directory, let's jump straight into uploading files.

func uploadObjectToDestination(client *sftp.Client, src, dest string) {
    // create destination file
    dstFile, err := client.OpenFile(remotePath+dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
    if err != nil {
        log.Fatal(err)
    }
    defer dstFile.Close()

    // open local file
    srcReader, err := os.Open(src)
    if err != nil {
        log.Fatal(err)
    }

    // copy source file to destination file
    bytes, err := io.Copy(dstFile, srcReader)
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("Total of %d bytes copied\n", bytes)
}

I also thought it'd be nice to notify myself when the screenshot is uploaded. I've used https://github.com/0xAX/notificator for this.

What the notification looks like

func showNotification(url string) {
    notify = notificator.New(notificator.Options{
        AppName: "Skrins",
    })
    notify.Push("Screenshot uploaded!", url, "", notificator.UR_NORMAL)
}

The last thing to do was to copy the url to the clipboard so I could immediately paste the link somewhere. I also relied on a 3rd party library for this. It can be found here: https://github.com/atotto/clipboard

func copyToClipboard(s string) {
    clipboard.WriteAll(s)
}

Not much more to it. Source is available on my GitHub https://github.com/slacki/skrins.

Now to make this app start with your system, at least on macOS, I had to create a .plist file and place it in ~/Library/LaunchAgents/com.skrins.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.skrins.agent</string>
    <key>LimitLoadToSessionType</key>
    <string>Aqua</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/slacki/projects/skrins/skrins</string>
        <string>-p</string>
        <string>/Users/slacki/Documents/screenshots</string>
        <string>-r</string>
        <string>slacki.io:22</string>
        <string>-ru</string>
        <string>i</string>
        <string>-pk</string>
        <string>/Users/slacki/.ssh/id_rsa</string>
        <string>-rp</string>
        <string>/home/i/i</string>
        <string>-url</string>
        <string>https://i.slacki.io</string>
    </array>
    <key>StandardErrorPath</key>
    <string>/dev/null</string>
    <key>StandardOutPath</key>
    <string>/dev/null</string>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>

That's it! Works as charm. Probably not worth the time, but hey, I've learned something about macOS's launchd. I'm still new to this.