When you can’t remember where you put things to ensure you don’t forget them

I’ve often said that some of the things I write here are more for my benefit than for yours. I figure that going through the effort of writing out a procedure and posting it will help me remember it. And if I can’t remember the procedure itself, at least I’ll remember that I wrote about it once, and I’ll be able to search the blog for it.

So it really pisses me off to realize that I’ve solved a problem, written about it, and then had absolutely no memory of it whatsoever when the problem reappeared. I am talking, unfortunately, about SnapClip again. You’re tired of it, I’m tired of it, but I need to get this out because there’s a much better solution to the “path to optipng” problem than what I came up with earlier today. You can find the answer on the Keyboard Maestro Wiki and, damningly, here on this very blog in a post from only four years ago.

I was finally reminded that this was a solved problem by a tweet from Vítor Galvão. He suggested starting my Python script with a change to the PATH environment variable: adding both /opt/homebrew/bin and /usr/local/bin. Having extra directories in your PATH doesn’t hurt anything, even if some of those directories don’t exist. Vítor does this with Ruby scripts he runs from within Alfred.

But in Keyboard Maestro there’s a better way.

  1. Open up the Variables preference pane and add a variable called ENV_PATH.
  2. Set it to whatever you would like your PATH to be when you run shell scripts within Keyboard Maestro. I use1

    /Users/drdrang/Dropbox/bin:
    /Users/drdrang/opt/anaconda/bin:
    /opt/homebrew/bin:
    /usr/local/bin:
    /usr/bin:
    /bin:
    /usr/sbin:
    /sbin
    

    Note that I’ve put each directory on its own line to make it easy to read. The real value of ENV_PATH has all those lines mashed together:

    /Users/drdrang/Dropbox/bin:/Users/drdrang/opt/anaconda/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
    
  3. Never worry about your PATH again. Every executable in those directories will be available to you as if you were working at the Terminal. No need to figure out absolute paths.

So my final (I hope) iteration on the SnapClip script is this:

 1: #!/usr/bin/python3
 2: 
 3: import Pashua
 4: import tempfile
 5: from PIL import Image
 6: import sys, os
 7: import subprocess
 8: import shutil
 9: from datetime import datetime
10: 
11: # Local parameters
12: type = "png"
13: localdir = os.environ['HOME'] + "/Pictures/Screenshots"
14: tf, tfname = tempfile.mkstemp(suffix='.'+type, dir=localdir)
15: bgcolor = (85, 111, 137)
16: border = 16
17: desktop = os.environ['HOME'] + "/Desktop/"
18: fname = desktop + datetime.now().strftime("%Y%m%d-%H%M%S." + type)
19: 
20: # Dialog box configuration
21: conf = b'''
22: # Window properties
23: *.title = Snapshot
24: 
25: # Border checkbox properties
26: bd.type = checkbox
27: bd.label = Add background
28: bd.x = 10
29: bd.y = 60
30: 
31: # Save file checkbox properties
32: sf.type = checkbox
33: sf.label = Save file to Desktop
34: sf.x = 10
35: sf.y = 35
36: 
37: # Default button
38: db.type = defaultbutton
39: db.label = Clipboard
40: 
41: # Cancel button
42: cb.type = cancelbutton
43: '''
44: 
45: # Capture a portion of the screen and save it to a temporary file.
46: status = subprocess.call(["screencapture", "-io", "-t", type, tfname])
47: 
48: # Exit if the user canceled the screencapture.
49: if not status == 0:
50:   os.remove(tfname)
51:   sys.exit()
52: 
53: # Open the dialog box and get the input.
54: dialog = Pashua.run(conf)
55: if dialog['cb'] == '1':
56:   os.remove(tfname)
57:   sys.exit()
58: 
59: # Add a desktop background border if asked for.
60: snap = Image.open(tfname)
61: if dialog['bd'] == '1':
62:   # Make a solid-colored background bigger than the screenshot.
63:   snapsize = tuple([ x + 2*border for x in snap.size ])
64:   bg = Image.new('RGBA', snapsize, bgcolor)
65:   bg.alpha_composite(snap, dest=(border, border))
66:   bg.save(tfname)
67: 
68: # Optimize the file.
69: subprocess.call(['optipng', '-quiet', tfname])
70: 
71: # Put the image on the clipboard.
72: subprocess.call(['impbcopy', tfname])
73: 
74: # Save to Desktop if asked for.
75: if dialog['sf'] == '1':
76:   shutil.copyfile(tfname, fname)
77: 
78: # Delete the temporary file
79: os.remove(tfname)

As you can see, there’s no more need to provide absolute paths to impbcopy or optipng in the “local parameters” section. They appear as short, simple strings in Lines 69 and 72. Setting up the proper environment in Keyboard Maestro, which I have known how to do for four years, solves everything and works on every Mac.

I promise I will stop writing about SnapClip even if I find another improvement.


  1. No, my username is not “drdrang,” it’s my real name.