alfred extra pane

Rich previews for Alfred script filters

139
5
Swift


Download
AlfredExtraPane

Spotight-like rich previews for Alfred workflows.

AlfredExtraPane is an experimental app that renders HTML from
quicklookurl of every item in the JSON produced by
Alfred Workflows.

How does it do it?

Alfred has an experimental “Press Secretary” to publish
macOS distributed notifications. These notifications
contain all the information needed to be able to show the extra pane.

Adding to workflow

Any workflow that produces items with quicklookurls that are either
HTML files or HTTP links automatically causes the extra pane to show
up with the quicklookurl loaded in it.

Alfred theme support

Alfred’s themes are stored in JSON files. Here’s a snippet from one such file:

{
  "alfredtheme" : {
    "result" : {
      "textSpacing" : 10,
      "subtext" : {
        "size" : 11,
        "colorSelected" : "#6E7073FF",
        "font" : "System Light",
        "color" : "#6E7073E5"}}}}

The pane converts this into CSS variables and injects them into the HTML.
The CSS looks like this:

:root {
  --result-textSpacing: 10px;
  --result-subtext-size: 11px;
  --result-subtext-colorSelected: "#6E7073FF";
  --result-subtext-font: "System Light";
  --result-subtext-color: "#6E7073E5";
}

As a workflow author, when you generate the HTML, use these
variables in it. The pane will make sure they’re injected.
Check out the toy example for a quick walk-through.

What’s not yet supported?

  • Configurability
    • Workflows should be able to opt out of the extra pane

Installation

Using a Script

Run the following command in the terminal:

curl -sL https://raw.githubusercontent.com/mr-pennyworth/alfred-extra-pane/main/install.sh | sh

Manual Installation

  • Download
    and extract AlfredExtraPane.app.zip.
  • Move the extracted AlfredExtraPane.app to /Applications.
  • Right-click on the app, and click Open:
  • Since this app isn’t signed with any developer certificate, macOS shows a
    warning. Click Cancel:
  • Again, right-click on the app, and click Open.
  • The warning dialog is different this time. It now allows to open the app.
    Click Open:

Features

  • To open a link displayed in the pane in the default browser, hold down
    the command key () and click on the link.

Configuration

The global pane(s) can be configured by editing
{/path/to}/Alfred.alfredpreferences/preferences/mr.pennyworth.AlfredExtraPane/config.json.
Similarly, pane(s) for individual workflows can be configured by editing
{workflow-dir}/extra-pane-config.json. If there are any workflow-specific
panes, the items produced from that workflow will not be shown in the global
panes.

Alternatively, you can use the Configure > Global menu to open the global
config JSON file in your default editor. Similarly,
Configure > [Workflow Name] opens the workflow-specific config file.

Configurable parameters are:

  • alignment (required):
    • horizontal
      • placement: left or right
      • width: width of the pane. If the width is so large that the
        pane doesn’t fit on the screen, it is automatically reduced so
        that the pane fills the entire horizontal space between the
        Alfred window and the edge of the screen.
      • minHeight (optional): minimum height of the pane. If not
        specified, the height of the pane is always same as the height
        of the Alfred window.
    • vertical
      • placement: top or bottom
      • height: height of the pane. If the height is so large that the
        pane doesn’t fit on the screen, it is automatically reduced so
        that the pane fills the entire vertical space between the
        Alfred window and the edge of the screen.
      • width (optional): width of the pane. If not specified, the
        width of the pane is always same as the width of the Alfred
        window. If the width is larger than the screen width, the pane
        is reduced so that it fits the screen.
  • customUserAgent (optional): User-Agent string for HTTP(S) URLs
  • customCSSFilename (optional): Name of the CSS file to be loaded
    in the pane. The file should be in the same directory as the JSON
    config file.
  • customJSFilename (optional): Name of the JavaScript file to be loaded
    in the pane. The file should be in the same directory as the JSON
    config file.
  • staticPaneConfig (optional):
    {"initURL": "https://fixed-url.com", "function": "jsFunctionName"}
    This pairs well with customJSFilename. In this mode, when the pane is
    created for the first time, it loads initURL. Then the workflow
    is expected to produce a text file containing the input it wants to
    send to the pane, and set the full path of this text file as the
    quicklookurl. The pane will execute the JavaScript function
    jsFunctionName with the contents of the text file as the argument.
  • mediaAutoplay (optional): true or false. If not specified, the
    default is false. If set to true, media elements in the pane will
    autoplay, if the webpage has configured them to autoplay.

Here’s an example with four panes configured:

[{
  "alignment" : {
    "horizontal" : {"placement" : "right", "width" : 300, "minHeight" : 400}}
}, {
  "alignment" : {
    "horizontal" : {"placement" : "left", "width" : 300, "minHeight" : 400}}
}, {
  "alignment" : {
    "vertical" : {"placement" : "top", "height" : 100}}
}, {
  "alignment" : {
    "vertical" : {"placement" : "bottom", "height" : 200}}
}]

Toy Example

Here’s a script filter that produces a result:

This is what you get when you run it:

Now, let’s attch an HTML preview to this result.
Create /tmp/one.html:

<html>
  <head>
  </head>
  <body>
    <h1> One </h1>
  </body>
</html>

Change the script filter:

cat << EOF
{"items" : [
  {"title": "One",
   "quicklookurl": "/tmp/one.html"}
]}
EOF

And the preview shows up!

Now let’s make the preview blend-in with the theme.
Here’s a snippet from relevant parts of Alfred’s theme:

{
  "alfredtheme" : {
    "result" : {
      "backgroundSelected" : "#00000054",
      "text" : {
        "size" : 22,
        "colorSelected" : "#E1E1E2FF",
        "font" : "System Light",
        "color" : "#A8A8ABFF"}}}}

Looking at the variable names in the above JSON, add the style section
to /tmp/one.html:

<html>
  <head>
    <style>
      h1 {
          color: var(--result-text-colorSelected);
      }
    </style>
  </head>
  <body>
    <h1> One </h1>
  </body>
</html>

Themed preview should show up:

Tutorial: Search Google as you type

Here’s a script filter that builds a Google search URL as you type:

q="$1"

function urlencode {
  echo -n "$1" \
   | /usr/bin/python3 -c "import sys, urllib.parse; print(urllib.parse.quote_plus(sys.stdin.read()),end='')"
}

url_q=$(urlencode "$1")

cat << EOF
{"items" : [
  {"title": "Search $q",
   "subtitle": "using google.com",
   "arg": "https://www.google.com/search?q=$url_q",
   "quicklookurl": "https://www.google.com/search?q=$url_q"}
]}
EOF

Connecting this script filter to an “Open URL” action
makes sure that pressing enter opens the URL in the default browser:

If AlfredExtraPane is running, the preview will show up as you type:

Customize Pane Placement

The placement of the pane on the right doesn’t seem to be the best
choice for this workflow. Let’s change it to the bottom. From the
Configure menu, open the config file for Google Search.

Since we’re configuring the pane for this workflow, for the first time,
the file will be empty:

Let’s change it to:

[{
  "alignment" : {"vertical" : {"placement" : "bottom", "height" : 600}}
}]

Restarting the app will show the pane at the bottom:

Show Mobile Version of Google with Custom User-Agent

We see that the desktop version of Google is shown in the preview,
where the text gets clipped on the right, and horizontal scrolling
is needed. Google’s mobile version is more suitable for us here.

Let’s change the User-Agent to a mobile browser’s:

[{
  "alignment" : {"vertical" : {"placement" : "bottom", "height" : 600}}, 
  "customUserAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Mobile Safari/537.36"
}]

Restarting the app will show the mobile version of Google.
Turns out, the mobile version respects macOS’s dark mode too:

Fine-Tune Webpages with Custom CSS

Precious screen real-estate is wasted by the Google logo and the search
bar. Let’s hide them with a custom CSS file.

Create style.css in the same directory as the JSON config file:

header {
  display: none;
}

Add the customCSSFilename key to the JSON config:

[{
  "alignment" : {"vertical" : {"placement" : "bottom", "height" : 600}},
  "customUserAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Mobile Safari/537.36",
  "customCSSFilename": "style.css"
}]

Restarting the app will show the pane with the Google logo and search
bar hidden:

Tutorial: Meta AI Image Generation as you type

In “Tutorial: Google as you type
we saw how to customize position of the pane and style of the webpage. We
could Google as we type, because the search query was a part of the
URL. That meant the workflow only needed to generate the URL, and the
pane would show the preview.

There are websites where the desired action isn’t controlled by the URL.
https://www.meta.ai/ is one such example. It has a text box where you
type the prompt (the prompt must begin with the word “imagine”), and the
AI generates an image based on that prompt, as you type. All this while,
the URL remains the same.

In this tutorial, we will build a workflow and configure the pane such
that the pane loads the URL once, and then listens for the prompt in the
workflow’s output, and simulates typing it in the text box. The end
result:

Prerequisites

  1. You have installed AlfredExtraPane.
  2. You have read
    Tutorial: Google as you type
    and are familiar with configuring the pane.
  3. You have a basic understanding of JavaScript (in the web context).
  4. You have a Facebook account to log in to Meta AI.

Configuring Static Pane

Here’s the pane configuration for the workflow:

[{
  "alignment" : {"vertical" : {"placement" : "bottom", "height" : 570}},
  "customJSFilename": "flashImagine.js",
  "staticPaneConfig": {
    "initURL": "https://www.meta.ai/",
    "function": "flashImagine"
  }
}]

This configuration tells the pane to load https://www.meta.ai/ once,
and insert the JavaScript from flashImagine.js into the loaded webpage.
Then, every time the workflow script filter runs, read query from file
whose path is set as the quicklookurl, and pass the query
to the JavaScript function flashImagine.

Here’s flashImagine.js, which defines the flashImagine function:

function flashImagine(query) {
  let textArea = document.getElementsByTagName('textarea')[0];

  // Calling textArea.value = query won't do as the webpage uses the
  // ReactJS framework.
  // See https://stackoverflow.com/a/46012210 for details.
  // The following code is conceptually equivalent to setting the value
  // of the text area:
  Object.getOwnPropertyDescriptor(
    window.HTMLTextAreaElement.prototype,
    'value'
  ).set.call(textArea, "imagine " + query);

  // When a user manually types in the text area, an "input" event
  // is generated. There's code in the webpage that listens to this
  // event to load the AI generated image. Since in this script, we
  // are setting the value in code (as opposed to manual entry by user),
  // we need to generate the "input" event in code too.
  textArea.dispatchEvent(
    new Event('input', {bubbles: true, cancelable: true})
  );
}

Workflow Script Filter

We have configured the pane, but we still need to create a script filter
that takes the query, writes it to a file, and then produces an item with
the path of the file as the quicklookurl:

q="$1"
input_file="/tmp/meta_ai_input.txt"

echo -n "$q" > "$input_file"

cat << EOF
{"items" : [
  {"title": "$q",
   "quicklookurl": "$input_file"}
]}
EOF

That’s it! That’s the entire workflow (also thrown in a hotkey trigger for
convenience):

Facebook Login

Running the workflow, we see that the query is being typed into the text
box, but the image isn’t showing up:

Meta AI requires you to log in to generate images. Click on the top
left corner of the pane, and log in to Meta AI:

Running the workflow now, we should see the image being generated:

Refining the Appearance

I don’t like how the text box and the padding around the image is taking
up so much space. I don’t need to see what’s in the textbox as it is the
same as what I’ve entered in Alfred.

When I looked into the HTML of the webpage, I couldn’t figure out how to
style the image so that it covers the entire pane. The <img> tags are
deeply buried into many <div> tags, whose style prevents us from
applying the absolute positioning to the <img> tag.

So, here’s a way to do that using JavaScript, where we grab the latest AI
generated image, insert it in a new <img> tag, which isn’t deeply nested
in the <div> tags, and thus, whose style we can control.

Create style.css in the same directory as the JSON config file, to
style the new <img> tag:

#finalImg {
  position: absolute !important;
  top: 0 !important;
  left: 0 !important;
  width: 100%;
  margin: 0px !important;
  padding: 0px !important;
  z-index: 99999 !important;
}

Update the JavaScript with code to create the stylable <img> tag:

// Create an image tag with "finalImg" as ID, and then
// every 20 milliseconds, look for the latest AI generated image,
// and copy it over to the "finalImg".
(function() {
  var img = document.getElementById("finalImg");
  if (img == null) {
    img = document.createElement("img");
    img.setAttribute("id", "finalImg");
    document.body.insertBefore(img, document.body.firstChild);
    setInterval(function() {
      // The latest image happens to be the last image tag in the webpage.
      let genImgSrc = [...document.getElementsByTagName('img')].reverse()[0].src;

      // handle the case where there aren't any AI generated images.
      if (genImgSrc.startsWith("data:")) {
        img.setAttribute('src', genImgSrc);

        // scroll to the top of the webpage, not really sure what's
        // causing the scrolling down in the first place, but always
        // scrolling to the top means we don't have to worry about it.
        window.scrollTo(0, 0);
      }
    }, 20);
  }
})();

function flashImagine(query) {
   ...
}

Add the CSS file to the JSON config:

[{
   "alignment" : {"vertical" : {"placement" : "bottom", "height" : 570}},
   "customJSFilename": "flashImagine.js",
   "customCSSFilename": "style.css",
   "staticPaneConfig": {
      "initURL": "https://www.meta.ai/",
      "function": "flashImagine"
   }
}]

Here’s the result:

The resultant workflow is hosted at
mr-pennyworth/alfred-meta-ai-flash-imagine.