Multi

从一组网站创建自定义的轻量级 macOS 应用程序。「Create a custom, lightweight macOS app from a group of websites」

Github星跟蹤圖

Multi

Create a custom, lightweight macOS app from a group of websites, complete with:

  • Native notifications, file uploads, and dialogs
  • Customization options with JSON, CSS, and JavaScript
  • CLI for creating and updating apps
  • Options for tabbed or floating windows

Watch me create a Slack clone from scratch in 30 seconds (high res video):

Table of contents

I've also written a few blog posts that discuss some of the decisions behind Multi:

Installation

The easiest method is to use Homebrew:

brew install --cask multi

Alternatively, you can manually download and run the latest .dmg from Releases.

JSON configuration

Multi apps store their configuration in a single JSON file.
If your app is named Test, then you'll find that file at /Applications/Multi/Test.app/Contents/Resources/config.json.
The JSON configuration uses the following top-level fields:

Field Name Type Description
tabs Array Titles and URLs of tabs for this app (Required)
windowed Boolean Start this app with each tab in its own window
alwaysNotify Boolean Show macOS notifications even if this app is currently focused
alwaysOnTop Boolean Position this app's window on top of all others
terminateWithLastWindow Boolean Determine if this app closes once all tabs/windows are closed
openNewWindowsInBackground Boolean Determines if browser app becomes active when opening external links
openNewWindowsWith String Override system default browser for external links—value is a bundle identifier like com.apple.Safari, com.google.Chrome, or com.mozilla.firefox

The tabs field is an array of objects with the following fields:

Field Name Type Description
url String Starting page for this tab (Required)
title String Name for this tab
customJs Array of Strings Custom JS URLs (see Custom JS/CSS)
customCss Array of Strings Custom CSS URLs (see Custom JS/CSS)
customCookies Array of Objects Custom cookies using HTTPCookiePropertyKey
basicAuthUser String User name credential for requests that use basic access authentication
basicAuthPassword String Password credential for requests that use basic access authentication
userAgent String Override the default WebKit user agent header

Here's a bare minimum example to recreate the Slack demo video above:

{ "tabs": [{ "url": "https://app.slack.com/client" }] }

Here's a fancier example that uses the optional fields referenced above:

{
  "tabs": [
    {
      "title": "Dancing",
      "url": "https://rc.kofi.sexy/bathroom-floss",
      "basicAuthUser": "user",
      "basicAuthPassword": "password",
      "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15"
    },
    {
      "title": "Walking",
      "url": "https://kofi.sexy/cel-shading",
      "customJs": [ "file:///Users/kofi/Documents/dotfiles/main/example.js" ],
      "customCss": [ "https://example.com/custom.css" ],
      "customCookies": [
        {
          "name": "login_token_tab",
          "value": "eyJoZWxsbyI6ICJ3b3JsZCJ9",
          "domain": ".example.com",
          "path": "/"
        }
      ]
    }
  ],
  "windowed": true,
  "alwaysNotify": true,
  "alwaysOnTop": true,
  "terminateWithLastWindow": true,
  "openNewWindowsInBackground": true,
  "openNewWindowsWith": "com.apple.Safari"
}

If your configuration file fails to decode, you can use the settings window to fix the issues.
Optional fields will always default to "empty" values (i.e. false, "", []).

Using the CLI: create-mac-app

You can create and update Multi apps entirely from the command-line with the included script.
In fact, the Multi configuration UI just runs this script under-the-hood!
The create-mac-app script takes its options as environment variables.
For instance, here's how you'd create a bare-minimum app named Test:

MULTI_APP_NAME='Test' /Applications/Multi.app/Contents/Resources/create-mac-app

When you open Test, you'll be greeted with the preferences window, where you can finish configuring your app.
If you'd like to configure your app entirely from the command-line, you can set any of the following variables:

MULTI_ICON_PATH PNG or ICNS path to icon image
MULTI_JSON_CONFIG See JSON configuration
MULTI_OVERWRITE Set to 1 to replace an existing Multi app with the same name

Custom JS/CSS

Multi lets you customize any site by injecting JavaScript and CSS on every page in your app.
Each custom JS/CSS file is specified with a URL, which gives you a few options for how you want to manage your customizations:

  1. Host your file online, and use its URL: ex. https://raw.githubusercontent.com/kofigumbs/dotfiles/master/example.js
  2. Reference a local file on your computer: ex. file:///Users/kofi/workspace/dotfiles/example.js
  3. Encode your script directly in the JSON using Data URIs: ex. data:,console.log%28%27Hello%2C%20from%20Multi%21%27%29%3B%0A

Custom JS/CSS is one of the most important parts of Multi.
It lets the main project stay small and focused, while letting you extend it with new features that fit your use case.
If you have a neat JS/CSS snippet, you'd like to share, please open an Issue or Pull Request!
Here are a few that have come up before:

Google seems to be doing some trickery here.
Instead of allowing the browser to handle the links, they use JS to open a blank popup window, then dynamically set the URL to google.com/url?q=REAL_URL_HERE.
Presumably all of this is so that they can track you for a few moments on your way out of their app.
Custom JS solution:

window.addEventListener("DOMContentLoaded", () => {
  const listener = e => e.stopPropagation();
  const query = () => document.querySelectorAll('a[target=_blank]').forEach(a => {
    a.removeEventListener('click', listener);
    a.addEventListener('click', listener, true);
  });
  setInterval(query, 400); // wait time between DOM queries, in milliseconds
});

Find in page

Multi doesn't include any search functionality (Cmd-F).
Custom JS solution:

window.addEventListener("DOMContentLoaded", () => {
  const highlightResults = (text, color) => {
    document.designMode = "on"; // https://stackoverflow.com/a/5887719
    var selection = window.getSelection();
    selection.collapse(document.body, 0);
    while (window.find(text)) {
      document.execCommand("HiliteColor", false, color);
      selection.collapseToEnd();
    }
    document.designMode = "off";
  };

  let mostRecentSearchText = "";
  const search = text => {
    highlightResults(mostRecentSearchText, "transparent");
    highlightResults(text, "rgb(255 255 1 / 50%)");
    mostRecentSearchText = text;
  };

  const input = document.createElement("input");
  input.placeholder = "Search...";
  input.style.padding = "10px 15px";
  input.style.fontSize = "15px";
  input.style.borderRadius = "3px";
  input.style.border = "solid 1px lightgray";

  const form = document.createElement("form");
  form.style.display = "none";
  form.style.position = "fixed";
  form.style.top = "15px";
  form.style.right = "15px";
  form.style.zIndex = "2147483647"; // https://stackoverflow.com/a/856569
  form.addEventListener("submit", e => {
    e.preventDefault();
    search(input.value);
  });

  const close = document.createElement("a");
  close.innerText = "⨯";
  close.href = "javascript:void(0)";
  close.style.fontSize = "30px";
  close.style.padding = "15px";
  close.style.textDecoration = "none";
  close.addEventListener("click", e => {
    e.preventDefault();
    search("");
    form.style.display = "none";
  });

  form.appendChild(input);
  form.appendChild(close);
  document.body.appendChild(form);

  document.addEventListener("keydown", event => {
    if (event.metaKey && event.key === "f") {
      event.preventDefault();
      form.style.display = "block";
      input.focus();
    }
  });
});

Drag-and-drop to open URLs

Say you have a URL outside of Multi (maybe in an email), and you want to open it in Multi.
Custom JS solution:

document.addEventListener("dragover", e => e.preventDefault());

Multi doesn't include any hover-to-preview-link-target functionality.
Custom CSS solution:

a:hover::after {
  content: attr(href);
  position: fixed;
  left: 4px;
  bottom: 4px;
  padding: 4px;
  font-size: 12px;
  font-family: -apple-system, BlinkMacSystemFont;
  font-weight: normal;
  color: black;
  background: ghostwhite;
  border: solid 1px black;
  border-radius: 1px;
}

Donating

主要指標

概覽
名稱與所有者kofigumbs/multi
主編程語言Swift
編程語言Swift (語言數: 5)
平台
許可證GNU General Public License v3.0
所有者活动
創建於2020-05-19 22:01:29
推送於2024-07-06 20:50:53
最后一次提交
發布數20
最新版本名稱v3.0.1 (發布於 )
第一版名稱v2.0.0 (發布於 )
用户参与
星數1.4k
關注者數14
派生數42
提交數308
已啟用問題?
問題數137
打開的問題數31
拉請求數7
打開的拉請求數0
關閉的拉請求數2
项目设置
已啟用Wiki?
已存檔?
是復刻?
已鎖定?
是鏡像?
是私有?