Open Warp Launch Configurations from Raycast and Alfred

Launch configurations from Warp are an incredible tool in our development tool belt. Let's get them working with both Raycast and Alfred so we can utilize them without opening Warp.

Joe Tannenbaum

I've been using Warp as my terminal for a couple of months now, and I have to say: I really like it.

One of my favorite features is their Launch Configurations:

Launch Configurations enables you to save your configuration of windows, tab, and panes, so that you can reopen the same set of windows, tab, and panes per project quickly.

This is basically a way more official version of what I was doing in iTerm2, so there's less hackery involved.

Warp launch configurations are stored as YAML files in ~/.warp/launch_configurations/, and you can launch them in Warp by hitting ⌘+ctrl+L:

Launching launch configs in Warp launchily
Launching launch configs in Warp launchily

That's... cool. For sure. But it's not super fast. I want to be able to dive into a project from wherever I am on my computer, which means that I need to integrate this into Alfred (or Raycast, depending on the day).

But Warp doesn't have a direct integration with either of these, and it doesn't seem to have a command line method of opening up launch configs either, so we'll have to reach into our JXA toolbox and just script exactly the same actions that we take manually, but automatically. Lezzdoit.

Hold On

Hey, Joe: I don't care how you built them. Just give me a link.

And to that I say: You got it, buddy.

Alfred Workflow

Raycast Extension

Building the Alfred Workflow

This is going to be pretty simple overall. We need to:

  • List the YAML files in ~/.warp/launch_configurations/

  • Grab the file base name and pass it to the JXA, running the commands in Warp as we normally would

We're going to list out the files in the launch configurations directory via a Script Filter because Alfred had trouble listing the contents of the dot directory. No problem:

1#!/usr/bin/osascript -l JavaScript
2 
3// Modified from: https://deanishe.net/alfred-workflows/Search%20PDFs.alfredworkflow
4 
5// Folder to search in
6const folder = '~/.warp/launch_configurations';
7// Only show files with this extension
8const extension = 'yaml';
9 
10// Return list of files in folder with given extension.
11function findFiles(folder, extension) {
12 const fm = $.NSFileManager.defaultManager;
13 const files = [];
14 
15 const root = $(folder).stringByExpandingTildeInPath;
16 const contents = fm.contentsOfDirectoryAtPathError(root, null);
17 
18 for (let i = 0; i < contents.count; i++) {
19 files.push(
20 root.stringByAppendingPathComponent(contents.objectAtIndex(i)),
21 );
22 }
23 
24 if (extension) {
25 return files.filter((p) => p.pathExtension.js == extension);
26 }
27 
28 return files;
29}
30 
31// Convert NSString path to an Alfred feedback item.
32function alfredItem(path) { ...
33 return {
34 title: path.lastPathComponent.js.replace('.yaml', ''),
35 subtitle: path.stringByAbbreviatingWithTildeInPath.js,
36 arg: path.stringByAbbreviatingWithTildeInPath.js,
37 uid: path.js,
38 valid: true,
39 type: 'file',
40 };
41}
42 
43function queryRegex(query) { ...
44 const pat = [];
45 const iter = query[Symbol.iterator]();
46 const char = iter.next();
47 
48 while (!char.done) {
49 pat.push(char.value);
50 char = iter.next();
51 }
52 
53 return new RegExp(pat.join('.*'), 'i');
54}
55 
56function toJSON(files) { ...
57 return JSON.stringify({ items: files.map(alfredItem) }, null, 2);
58}
59 
60function run(argv) {
61 const query = argv.length ? argv[0] : null;
62 
63 const files = findFiles(folder, extension);
64 
65 if (!query) {
66 return toJSON(files);
67 }
68 
69 const regex = queryRegex(query);
70 
71 return toJSON(
72 files.filter((p) => p.lastPathComponent.js.match(regex) !== null),
73 );
74}

Then we do some transforming on Alfred's site (strip out the path and extension) and pass the result to our JXA:

1function run() {
2 const query = '{query}';
3 const se = Application('System Events');
4 
5 Application('Warp').activate();
6 
7 se.keystroke('l', { using: ['command down', 'control down'] });
8 se.keystroke(query);
9 se.keyCode(36);
10}

And there we have it, we're opening launch configs via Alfred with this simple workflow:

5 workflow blocks = countless hours saved. Ok, maybe minutes. But still.
5 workflow blocks = countless hours saved. Ok, maybe minutes. But still.

Building the Raycast Extension

Principle is the same as the Alfred workflow, with some adjustments for Raycast:

1 ...
2import { ActionPanel, Action, List, closeMainWindow } from '@raycast/api';
3import { useEffect, useState } from 'react';
4import fs from 'node:fs';
5import path from 'node:path';
6import os from 'node:os';
7import { runJxa } from 'run-jxa';
8 
9export default function Command() {
10 const [searchText, setSearchText] = useState('');
11 const [results, setResults] = useState<SearchResult[]>([]);
12 
13 useEffect(() => {
14 const fullPath = path.join(os.homedir(), '.warp/launch_configurations');
15 
16 fs.readdir(fullPath, (error, files) => {
17 if (error) {
18 console.log(error);
19 }
20 
21 setResults(
22 files.map((file) => ({
23 name: file.replace('.yaml', ''),
24 path: path.join(fullPath, file),
25 })),
26 );
27 });
28 }, []);
29 
30 return (
31 <List
32 isLoading={results.length === 0}
33 onSearchTextChange={setSearchText}
34 searchBarPlaceholder="Searching for launch configurations..."
35 throttle
36 >
37 <List.Section title="Results" subtitle={results?.length + ''}>
38 {results
39 ?.filter((f) => f.name.includes(searchText.toLowerCase()))
40 .map((searchResult) => (
41 <SearchListItem
42 key={searchResult.name}
43 searchResult={searchResult}
44 />
45 ))}
46 </List.Section>
47 </List>
48 );
49}
50 
51function SearchListItem({ searchResult }: { searchResult: SearchResult }) {
52 const appleScript = `const se = Application('System Events');
53 Application('Warp').activate();
54 se.keystroke('l', { using: ['command down', 'control down'] });
55 se.keystroke('${searchResult.name}');
56 se.keyCode(36);`;
57 
58 return (
59 <List.Item
60 title={searchResult.name}
61 actions={
62 <ActionPanel>
63 <ActionPanel.Section>
64 <Action
65 title="Launch"
66 onAction={() => {
67 runJxa(appleScript);
68 closeMainWindow();
69 }}
70 />
71 </ActionPanel.Section>
72 <ActionPanel.Section>
73 <Action.ShowInFinder
74 title="Reveal in Finder"
75 path={searchResult.path}
76 shortcut={{ modifiers: ['cmd'], key: '.' }}
77 />
78 <Action.Open
79 title="Edit Launch Configuration"
80 target={searchResult.path}
81 shortcut={{ modifiers: ['cmd'], key: 'o' }}
82 />
83 </ActionPanel.Section>
84 </ActionPanel>
85 }
86 />
87 );
88}
89 
90interface SearchResult {
91 name: string;
92 path: string;
93}

Super simple, really. We've also added some extra actions that allow you to view the selected workflow in the Finder or edit it directly. Bing bang boom.

Wrapping Up

Hope you found this useful! Here are those links again if you're interested in using either of them:

Alfred Workflow

Raycast Extension

(Excellent) syntax highlighting provided by Torchlight