Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/srsholmes/yal

Yet Another Launcher
https://github.com/srsholmes/yal

Last synced: 3 months ago
JSON representation

Yet Another Launcher

Awesome Lists containing this project

README

        

# 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](https://raw.githubusercontent.com/srsholmes/yal/develop/apps/yal/resources/web-plugin.png 'Yal Screenshot')
![Yal Screenshot](https://raw.githubusercontent.com/srsholmes/yal/develop/apps/yal/resources/react-plugin.png 'Yal React App Screenshot')

## Install Yal

Releases are coming soon, but for now you can install Yal by cloning this repo and running the following commands:

```bash
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](https://tauri.app/v1/guides/getting-started/prerequisites/).

Then run the following command to build the app:

```bash
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](https://raw.githubusercontent.com/srsholmes/yal/develop/apps/yal/resources/dev-tools-enabled.png 'Yal React App With Dev Tools Enabled')

## Plugins

To see the growing list of available plugins, please visit the [yal-plugins](https://github.com/srsholmes/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:

```javascript
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) => 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:

```typescript
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](https://raw.githubusercontent.com/srsholmes/yal/develop/apps/yal/resources/color.png '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`](https://github.com/srsholmes/yal-solid/tree/develop/apps/yal#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.

```javascript
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:

```typescript
type ActionArgs = {
setState: (state: PluginResult) => void;
item: ResultLineItem;
searchText?: string;
pluginActions: PluginActions;
};
```

| Key | Type | Function |
| ------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| setState | `(state: PluginResult) => 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` | This will be the item that the user clicks selects. Additional metadata (``) 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:

```typescript
import * as index from '@tauri-apps/api';

type PluginActions = {
copyToClipboard: (text: string) => Promise;
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;
shellCommand: YalCommand;
open: YalCommand;
appleScript: ({ command }: { command: string }) => Promise;
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](https://tauri.app/v1/api/js/)

## Apps

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

![Example of an App plugin](https://raw.githubusercontent.com/srsholmes/yal/develop/apps/yal/resources/google-maps-example-app.png 'Example of an App plugin')

Source code available here: [Google Maps Plugin](https://github.com/srsholmes/yal-plugins/tree/main/plugins/google-maps)

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`

```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(




Hello World from react!



The input text (without the keyword is): {args.text}





);
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.

```tsx
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(




This is a basic example of how to use the Yal API.



Please click the button below to open a file dialog. Once you have
selected a file, it will be displayed below the button.








);
return { appNode };
};

export const config: YalPluginsConfig = {
keywords: 'file',
filter: false,
isApp: true,
};

export default testReactApp;
```

`FileReader.tsx`

```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 (



Choose file...

{filePath && (



)}

);
};
```

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](https://raw.githubusercontent.com/srsholmes/yal/develop/apps/yal/resources/plugins-installed.png 'See installed plugins')

## Themes

![Example of themes in Yal](https://raw.githubusercontent.com/srsholmes/yal/develop/apps/yal/resources/themes.png 'Example of themes')

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:

```json
{
"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.

- [Alfred](https://www.alfredapp.com/)
- [Raycast](https://raycast.com/)
- [ScriptKit](https://scriptkit.app/)

License

- MIT