/yal

Yet Another Launcher

Primary LanguageTypeScript

Yal

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.


Screenshots

Yal Screenshot Yal Screenshot

Install Yal

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

Developing Yal Locally

yarn dev

Dev Tools

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'.

Yal Screenshot With Dev tools

Plugins

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;

Plugin arguments

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.

Plugin configuration

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;
};

keywords: string | string[]

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.

Example of a root level plugin

debounce: boolean | number

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.

throttle: boolean | number

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.

isApp: boolean

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.

filter: boolean

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.

Plugin Actions

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

Plugin Actions

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

Apps

Yal gives you the ability to run full applications inside of Yal.

Example of an App plugin

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.

Installed Plugins

You can see all a list of currently installed plugins with keywords by searching for plugins in Yal.

See Installed plugins

Themes

Example of themes 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.

Configuration

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.

Accessibility Permissions

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.

Issues

  • 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.

Contributing

  • 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

Alternatives / Inspired by

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