Yal (Yet Another Launcher) is a launcher app similar to Alfred, Raycast, ScriptKit, Spotlight, and many others. Yal is designed to provide users with a powerful and efficient way to launch applications and perform actions on Mac OS.
Yal has been designed with the goal of being the simple, powerful and fast. With an emphasis on speed and efficiency, Yal is perfect for users who want to be able to quickly perform tasks and access the information they need, without having to navigate through cluttered menus or slow-loading interfaces.
Releases are coming soon, but for now you can install Yal by cloning this repo and running the following commands:
yarn
# install rust or update rust (depending on if you have it installed already)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # Install
rustup update # Update
Follow the prerequisites for setting up tauri.
Then run the following command to build the app:
yarn workspace @apps/yal prod:install
yarn dev
Yal has dev tools enabled by default, in both production and development builds. This allows really quick development of new plugins with tools we are all familiar with. To open the dev tools, right click anywhere in the Yal window and select 'Inspect Element'.
To see the growing list of available plugins, please visit the yal-plugins repo.
Yal can be extended via the use of plugins. Plugins are functions which display a list of results to the user based on their input.
A plugin must export a default function in order for it to work.
A simple hello world
plugin would be as follows:
const plugin = async (args) => {
args.setState({
heading: 'Hello World',
state: [
{
name: 'This is the first result',
description: 'This is the first result description',
},
],
});
};
export default plugin;
Please note, TS types are available as PluginArgs
type in the @yal-app/types
package.
Plugins are called with an arguments object which contains the following properties:
Key | Type | Function |
---|---|---|
setState | (state: PluginResult<T>) => void |
The setState function can be called to set a list of results. |
text | string |
The text property contains the text that the user has typed into the search box. |
system | { apps: AppEntry[]; } |
The system object contains an app key with a list of all the apps installed on the system. |
utils | <{ setToast: (args: YalToast | null) => void; } |
The utils object contains a setToast function which can be used to set a toast message. |
appNode | HTMLElement |
The appNode is an optional property which will be set if the plugin is an app. This is the DOM node that the app should be mounted in to. |
Plugins can export an optional comnfigtation object of type YalPluginsConfig
with number of different optional keywords which allows you to customise how the plugin works.
A yal configuration object looks like this:
type YalPluginsConfig = {
isApp?: boolean;
keywords?: string | string[];
filter?: boolean;
debounce?: boolean | number;
throttle?: boolean | number;
keepOpen?: boolean;
};
The keywords array will tell Yal to only call the plugin after one of the keywords has been typed into the search box.
If no keywords are specified, the plugin will be run regardless of what word is typed into the search box. This can be very helpful if you want plugins to be available from the 'root' popup search window for example if you want to paste in a hex value to convert to different color formats.
If a plugin exports the boolean debounce
as true
, this will tell Yal to debounce the input to the plugin when it is called. This is particularly useful for asynchronous plugins that query an API to prevent spamming of requests. You can also provide a number to debounce to specify the time in milliseconds to debounce the input.
If a plugin exports the boolean throttle
as true
, this will tell Yal to throttle the input to the plugin when it is called. This is particularly useful for asynchronous plugins that query an API to prevent spamming of requests. You can also provide a number to throttle to specify the time in milliseconds to throttle the input.
The isApp
export can be used to tell Yal that the plugin is a standalone application. This will not render the traditional list of items, but instead it is up to the plugin Author to render an application as they want. This is very powerful for plugin authors as it allows full flexibility inside of Yal for building whatever you wish.
For more infomation see the Apps
section.
The filter
export is a boolean which will allow Yal to automatically filter the results of the plugin based on what the user is searching for. For example if I have 3 results, foo
, bar
and baz
, and the user searches ba
, if the filter option is set to true
the list will be filtered down to bar
and baz
.
The search results are based on a fuzzy search.
You will want to have filter = false
when developing plugins which are going to be used from the top level screen, so the text does not filter their results.
A plugin result can have an optional action
property which will be called when the user clicks on the item, or selects the item and presses the return key. The selected item will be passed to the action function.
const plugin = async (args) => {
args.setState({
heading: 'Hello World',
state: [
{
name: 'This is the first result',
description: 'This is the first result description',
metadata: {
hello: 'world',
},
},
],
action: async ({ item, pluginActions }: ActionArgs) => {
await yal.copyToClipboard(item.metadata.hello); // Copies 'world' to the clipboard
},
});
};
export default plugin;
The type signature for the action function is:
type ActionArgs<T> = {
setState: <T>(state: PluginResult<T>) => void;
item: ResultLineItem<T>;
searchText?: string;
pluginActions: PluginActions;
};
Key | Type | Function |
---|---|---|
setState | (state: PluginResult<T>) => void |
The setState function can be called to set a list of results. |
searchText | string |
The text property contains the text that the user has typed into the search box. |
item | ResultLineItem<T> |
This will be the item that the user clicks selects. Additional metadata (<T> ) can be stored on each result which will be passed through to the action. This is really handy for passing through things you dont want the user to see e.g UUIDs etc. |
pluginActions | PluginActions |
These are various functions and utilities which can be called to interact with the users system |
The type signature for the plugin actions is:
import * as index from '@tauri-apps/api';
type PluginActions = {
copyToClipboard: (text: string) => Promise<void>;
fs: typeof index.fs;
app: typeof index.app;
dialog: typeof index.dialog;
globalShortcut: typeof index.globalShortcut;
http: typeof index.http;
notification: typeof index.notification;
path: {
convertFileSrc: typeof convertFileSrc;
appDir: typeof index.path.appDir;
appConfigDir: typeof index.path.appConfigDir;
appDataDir: typeof index.path.appDataDir;
appLocalDataDir: typeof index.path.appLocalDataDir;
appCacheDir: typeof index.path.appCacheDir;
appLogDir: typeof index.path.appLogDir;
audioDir: typeof index.path.audioDir;
cacheDir: typeof index.path.cacheDir;
configDir: typeof index.path.configDir;
dataDir: typeof index.path.dataDir;
desktopDir: typeof index.path.desktopDir;
documentDir: typeof index.path.documentDir;
downloadDir: typeof index.path.downloadDir;
executableDir: typeof index.path.executableDir;
fontDir: typeof index.path.fontDir;
homeDir: typeof index.path.homeDir;
localDataDir: typeof index.path.localDataDir;
pictureDir: typeof index.path.pictureDir;
publicDir: typeof index.path.publicDir;
resourceDir: typeof index.path.resourceDir;
runtimeDir: typeof index.path.runtimeDir;
templateDir: typeof index.path.templateDir;
videoDir: typeof index.path.videoDir;
logDir: typeof index.path.logDir;
BaseDirectory: typeof index.path.BaseDirectory;
sep: typeof index.path.sep;
delimiter: typeof index.path.delimiter;
resolve: typeof index.path.resolve;
normalize: typeof index.path.normalize;
join: typeof index.path.join;
dirname: typeof index.path.dirname;
basename: typeof index.path.basename;
isAbsolute: typeof index.path.isAbsolute;
};
process: typeof index.process;
shell: {
run: ({
binary,
args,
}: {
binary: string;
args?: string[];
}) => Promise<index.shell.ChildProcess>;
shellCommand: YalCommand;
open: YalCommand;
appleScript: ({ command }: { command: string }) => Promise<void>;
Command: typeof index.shell.Command;
};
windowUtils: typeof index.window;
};
As you can see we have access to the entire Tauri API, as well as some additional utilities. For more information on the Tauri API, please see the Tauri API Docs
Yal gives you the ability to run full applications inside of Yal.
Source code available here: Google Maps Plugin
Applications can be written in vanilla JS or any framework (react / svelte etc) and will have full access to the Yal API.
When creating an app, Yal will pass in a DOM node for you to mount your application in to.
You can use tailwind to style your apps and it will work out of the box, or you can use something else like css modules / CSS in JS.
Let's start by making a basic app in react:
index.tsx
import { YalPluginsConfig, YalReactAppPlugin } from '@yal-app/types';
import React from 'react';
import { createRoot } from 'react-dom/client';
let root;
const testReactApp: YalReactAppPlugin = (args) => {
const { appNode } = args;
if (!root) {
root = createRoot(appNode);
}
root.render(
<React.StrictMode>
<div className="mx-auto max-w-2xl py-32 sm:py-48 lg:py-56">
<div className="text-center">
<h1 className="text-4xl font-bold tracking-tight text-white sm:text-6xl">
Hello World from react!
</h1>
<p className="mt-6 text-lg leading-8 text-gray-300">
The input text (without the keyword is): {args.text}
</p>
</div>
</div>
</React.StrictMode>
);
return { appNode };
};
export const config: YalPluginsConfig = {
keywords: 'react',
filter: false,
isApp: true,
throttle: false,
debounce: 5000,
};
export default testReactApp;
The most important thing to note here, is the app is exported with the config option of isApp
being true
. The will tell yal to run the app as its own standalone application. This basic app will be available from the root search window, when the user types react
.
Next, let's make something using Yals API.
Here is an example of opening an image on the users computer, and viewing it in Yal.
import { YalPluginsConfig, YalReactAppPlugin } from '@yal-app/types';
import React from 'react';
import { createRoot } from 'react-dom/client';
import { FileReader } from './FileReader';
let root;
const testReactApp: YalReactAppPlugin = (args) => {
const { appNode } = args;
if (!root) {
root = createRoot(appNode);
}
root.render(
<React.StrictMode>
<div className="mx-auto max-w-2xl py-32 sm:py-48 lg:py-56">
<div className="text-center">
<h1 className="text-4xl font-bold tracking-tight text-white sm:text-6xl">
This is a basic example of how to use the Yal API.
</h1>
<p className="mt-6 text-lg leading-8 text-gray-300">
Please click the button below to open a file dialog. Once you have
selected a file, it will be displayed below the button.
</p>
</div>
<div className="flex items-center">
<FileReader />
</div>
</div>
</React.StrictMode>
);
return { appNode };
};
export const config: YalPluginsConfig = {
keywords: 'file',
filter: false,
isApp: true,
};
export default testReactApp;
FileReader.tsx
import React, { useState } from 'react';
export const FileReader = () => {
const [filePath, setFilePath] = useState(null);
function handleFileChoose(e) {
e.preventDefault();
yal.dialog
.open({
multiple: false,
filters: [{ name: 'Images', extensions: ['png', 'jpg', 'jpeg'] }],
})
.then((path) => {
if (path) {
const asset = yal.path.convertFileSrc(path as string);
setFilePath(asset);
}
});
}
return (
<div>
<button
className="bg-red-500 text-white p-2"
onClick={handleFileChoose}
>
Choose file...
</button>
{filePath && (
<div className="mt-4">
<img src={filePath} />
</div>
)}
</div>
);
};
Hopefully this gives you some insight as to how powerful apps can be in Yal.
We're looking forward to seeing what the community comes up with.
You can see all a list of currently installed plugins with keywords by searching for plugins
in Yal.
Yal is completely themeable. You can see all available themes by searching for themes
in Yal and selecting one.
You can install all the themes by running yarn install:themes
from the route directory.
Themes can be found in a directory called themes
, which is located in the Yal folder ~/.yal
.
Themes are configured using JSON using a set of key value pairs. The keys represent specific UI elements and the values are tailwind classes to style them.
A full set of themable UI elements can be seen below below.
Here is an example of a theme:
{
"yal-wrapper": "flex flex-col h-screen bg-[#1B2C3F]",
"app-wrapper": "bg-[#1B2C3F]",
"main-wrapper": "min-h-screen",
"main-input": "bg-[#1B2C3F] block w-full p-3 text-white placeholder-white focus:ring-0 sm:text-sm",
"main-input-wrapper": "sticky bg-[#1B2C3F] top-0 mx-auto w-full transition-all grid items-center",
"search-icon": "hidden opacity-100 pointer-events-none top-0.5 left-0.5 h-5 w-5 text-[#F5CF03] p-2 h-10 w-10",
"result-heading": "px-3 py-2 text-md font-semibold text-[#FFC600]",
"results-wrapper": "",
"results-wrapper-height": "overflow-scroll pb-10",
"result-item": "group mx-4 flex cursor-pointer overflow-hidden p-3",
"result-item-info-wrapper": "ml-4 flex-auto",
"highlight": "bg-[#1F4661FF] group highlight",
"result-item-name": "text-sm font-medium text-[#CDD6DB] group-[.highlight]:text-[#FFFFFF]",
"result-item-description": "overflow-hidden text-sm text-[#606B70] group-[.highlight]:text-[#FFC600]",
"result-item-icon": "flex h-10 w-10 flex-none items-center justify-center overflow-hidden rounded-full",
"result-item-app-wrapper": "relative rounded-b-md",
"alert-wrapper": "h-full absolute w-full top-0 items-end flex justify-end",
"info": "bg-[#B2D7FF] group info",
"warning": "bg-[#FFC600] group warning",
"success": "bg-[#1F4661] group success",
"error": "bg-[#0E2232] group error",
"alert": "gap-4 grid text-[#FFFFFF] alert bottom-3 w-1/2 right-0 mt-3 transition-opacity ease-in-out duration-800 p-3 grid-cols-[auto_1fr]"
}
In the future I want to offer themimg via CSS files.
The config file for Yal is located at ~/.yal/config.json
. A typical config file looks like this
{
"theme": "yal-default",
"directories": [
"/Users/srsholmes/Work"
]
}
There may be some additional properties in the config file, but these are the main ones. These properties are used to configure certain aspects of Yal, and will be extended in the future.
Currently there is a bug where requested permissions for Yal do not work properly. In order to get permissions for Yal, you will need to go to System Preferences > Privacy & Security > Accessibility
and add Yal to the list of apps that have access to accessibility. If Yal is already in the list, remove Yal and add it again.
- I'm always looking for help with Yal, and I really value user feedback. If you have any issues, please feel free to create an issue in the github repo, and I'll do my best to help you out.
- If you spot a bug and would like to contribute, please feel free to to create a PR with the fix.
- For new features, please create an issue with the feature label and we can discuss implementation / need for feature
All of these projects have heavily inspired the development of Yal, and I would highly recommend checking them out if you are looking for a launcher app.
License
- MIT