Rich previews for Alfred script filters
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.
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.
Any workflow that produces items with quicklookurl
s that are either
HTML files or HTTP links automatically causes the extra pane to show
up with the quicklookurl
loaded in it.
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.
Run the following command in the terminal:
curl -sL https://raw.githubusercontent.com/mr-pennyworth/alfred-extra-pane/main/install.sh | sh
AlfredExtraPane.app.zip
.AlfredExtraPane.app
to /Applications
.Open
: Cancel
: Open
.Open
: ⌘
) and click on the link.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 theminHeight
(optional): minimum height of the pane. If notvertical
placement
: top
or bottom
height
: height of the pane. If the height is so large that thewidth
(optional): width of the pane. If not specified, thecustomUserAgent
(optional): User-Agent string for HTTP(S) URLscustomCSSFilename
(optional): Name of the CSS file to be loadedcustomJSFilename
(optional): Name of the JavaScript file to be loadedstaticPaneConfig
(optional):{"initURL": "https://fixed-url.com", "function": "jsFunctionName"}
customJSFilename
. In this mode, when the pane isinitURL
. Then the workflowquicklookurl
. The pane will execute the JavaScript functionjsFunctionName
with the contents of the text file as the argument.mediaAutoplay
(optional): true
or false
. If not specified, thefalse
. If set to true
, media elements in the pane willHere’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}}
}]
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:
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:
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:
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:
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:
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:
AlfredExtraPane
.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})
);
}
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):
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:
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
.