chopstick
All widgets

Color Picker

Comes with Chopstick

Pick colors anywhere on the screen with the eyedropper. Each one is copied as a hex code and added to a palette.

Already installed? It's in your widget library.

What it runs

A widget is a folder of plain files. Here is all of its code.

widget.json
{
  "id": "color-picker",
  "name": "Color Picker",
  "icon": "icon.png",
  "description": "Pick colors anywhere on the screen with the eyedropper. Each one is copied as a hex code and added to a palette.",
  "anytime": false,
  "initial": "picking",
  "states": {
    "picking": {
      "task": true,
      "on_enter": [ { "run": "pick.sh", "then": "copied", "else": "cancelled" } ],
      "compact": { "leading": { "symbol": "eyedropper" }, "trailing": "spinner" }
    },
    "copied": {
      "task": true,
      "open": true,
      "dismiss_after": 8,
      "on_enter": [ { "copy": "{{result.hex}}" } ],
      "compact": { "leading": { "symbol": "circle.fill", "tint": "{{result.hex}}" }, "trailing": { "text": "{{result.hex}}" } },
      "expanded": {
        "size": "compact",
        "header": { "symbol": "circle.fill", "tint": "{{result.hex}}", "title": "{{result.hex}} copied" },
        "blocks": [
          { "list": { "from": "result.palette", "limit": 8, "title": "{{item.hex}}", "subtitle": "rgb({{item.rgb}})", "image": "{{item.image}}",
                      "do": [ { "copy": "{{item.hex}}" }, "dismiss" ] } },
          { "row": [
            { "button": { "symbol": "doc.on.doc", "label": "Copy All", "do": [ { "copy": "{{result.all}}" }, "dismiss" ] } },
            { "button": { "symbol": "trash", "label": "Clear", "style": "plain", "do": [ { "run": "clear.sh" }, "dismiss" ] } }
          ] }
        ]
      },
      "keys": [
        { "key": "cmd+p", "label": "Pick Another", "do": [ { "go": "picking" } ] },
        { "key": "cmd+c", "label": "Copy All", "when": "nothing_copied", "do": [ { "copy": "{{result.all}}" }, "dismiss" ] }
      ]
    },
    "cancelled": {
      "dismiss_after": 2,
      "compact": { "leading": { "symbol": "eyedropper" }, "trailing": { "text": "Cancelled" } }
    }
  },
  "preview": {
    "states": [ "copied", "cancelled" ],
    "data": { "result": { "hex": "#3A7BFF", "rgb": "58, 123, 255", "all": "#3A7BFF\n#FF6B35",
                          "palette": [ { "hex": "#3A7BFF", "rgb": "58, 123, 255" }, { "hex": "#FF6B35", "rgb": "255, 107, 53" } ] } }
  }
}
clear.sh
# Empties the palette.
rm -f "$CHOPSTICK_STORAGE/palette.json"
palette.js
ObjC.import('AppKit')

// Puts the color in argv[0] (#RRGGBB) at the front of the palette kept in CHOPSTICK_STORAGE
// (newest first, up to 8), and prints the palette with a swatch image for each color.
function run(argv) {
    const storage = $.NSProcessInfo.processInfo.environment.objectForKey('CHOPSTICK_STORAGE').js
    const file = storage + '/palette.json'
    const saved = $.NSString.stringWithContentsOfFileEncodingError(file, $.NSUTF8StringEncoding, null)
    const hexes = saved.isNil() ? [] : JSON.parse(saved.js)
    const hex = argv[0].toUpperCase()
    const palette = [hex].concat(hexes.filter(h => h !== hex)).slice(0, 8)
    $(JSON.stringify(palette)).writeToFileAtomicallyEncodingError(file, true, $.NSUTF8StringEncoding, null)

    const colors = palette.map(h => {
        const rgb = [1, 3, 5].map(i => parseInt(h.slice(i, i + 2), 16))
        return { hex: h, rgb: rgb.join(', '), image: swatch(storage, h, rgb) }
    })
    return JSON.stringify({ hex: hex, rgb: colors[0].rgb, palette: colors, all: palette.join('\n') })
}

function swatch(storage, hex, rgb) {
    const path = storage + '/swatches/' + hex.slice(1) + '.png'
    if ($.NSFileManager.defaultManager.fileExistsAtPath(path)) return path
    $.NSFileManager.defaultManager.createDirectoryAtPathWithIntermediateDirectoriesAttributesError(storage + '/swatches', true, $(), null)
    const image = $.NSImage.alloc.initWithSize($.NSMakeSize(40, 40))
    image.lockFocus
    $.NSColor.colorWithSRGBRedGreenBlueAlpha(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1).setFill
    $.NSRectFill($.NSMakeRect(0, 0, 40, 40))
    image.unlockFocus
    $.NSBitmapImageRep.imageRepWithData(image.TIFFRepresentation)
        .representationUsingTypeProperties($.NSBitmapImageFileTypePNG, $()).writeToFileAtomically(path, true)
    return path
}
pick.js
ObjC.import('AppKit')
ObjC.import('stdlib')

// Shows the system eyedropper; prints the clicked color as #RRGGBB. Esc exits 1.
function run() {
    $.NSApplication.sharedApplication.setActivationPolicy($.NSApplicationActivationPolicyAccessory)
    let picked = null, done = false
    $.NSColorSampler.alloc.init.showSamplerWithSelectionHandler(function (color) {
        if (color && !color.isNil()) {
            const c = color.colorUsingColorSpace($.NSColorSpace.sRGBColorSpace)
            picked = [c.redComponent, c.greenComponent, c.blueComponent]
                .map(n => Math.round(n * 255).toString(16).padStart(2, '0').toUpperCase()).join('')
        }
        done = true
    })
    while (!done) $.NSRunLoop.currentRunLoop.runUntilDate($.NSDate.dateWithTimeIntervalSinceNow(0.05))
    if (!picked) $.exit(1)
    return '#' + picked
}
pick.sh
# Shows the system eyedropper and adds the color clicked to the palette. Esc exits 1.
hex=$(osascript -l JavaScript pick.js) || exit 1
osascript -l JavaScript palette.js "$hex"