{"id":13554704,"url":"https://github.com/kofigumbs/multi","last_synced_at":"2025-04-08T11:07:51.625Z","repository":{"id":39083012,"uuid":"265377829","full_name":"kofigumbs/multi","owner":"kofigumbs","description":"Create custom, lightweight macOS apps from websites","archived":false,"fork":false,"pushed_at":"2024-07-06T20:50:53.000Z","size":16877,"stargazers_count":1339,"open_issues_count":31,"forks_count":42,"subscribers_count":14,"default_branch":"main","last_synced_at":"2025-04-01T09:27:30.642Z","etag":null,"topics":["cli","macos","swift","webkit","webview"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kofigumbs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"COPYING","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-05-19T22:01:29.000Z","updated_at":"2025-03-27T07:53:20.000Z","dependencies_parsed_at":"2023-01-24T02:30:44.840Z","dependency_job_id":"74b929b8-ed72-48da-a169-856f634c8295","html_url":"https://github.com/kofigumbs/multi","commit_stats":null,"previous_names":[],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kofigumbs%2Fmulti","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kofigumbs%2Fmulti/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kofigumbs%2Fmulti/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kofigumbs%2Fmulti/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kofigumbs","download_url":"https://codeload.github.com/kofigumbs/multi/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247829491,"owners_count":21002995,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","macos","swift","webkit","webview"],"created_at":"2024-08-01T12:02:53.211Z","updated_at":"2025-04-08T11:07:51.605Z","avatar_url":"https://github.com/kofigumbs.png","language":"Swift","readme":"\n\u003cimg align=\"left\" src=\"./Docs/logo.png\" alt=\"Multi logo\" style=\"width: 80px; height: 80px;\" width=\"80px\" height=\"80px\" /\u003e\n\n# Multi\n\nCreate a custom, lightweight macOS app from a group of websites, complete with:\n\n - Native notifications, file uploads, and dialogs\n - Customization options with JSON, CSS, and JavaScript\n - CLI for creating and updating apps\n - Options for tabbed or floating windows\n\nWatch me create a Slack clone from scratch in 30 seconds (\u003ca href=\"https://kofi.sexy/slack-app-fewer-resources/demo.mp4\" target=\"_blank\"\u003ehigh res video\u003c/a\u003e):\n\n\u003cp align=\"center\"\u003e\n \u003cimg src=\"./Docs/demo.gif\" alt=\"Demo GIF\"\u003e\n\u003c/p\u003e\n\n\n## Table of contents\n\n - [Installation](#installation)\n - [JSON configuration](#json-configuration)\n - [Using the CLI: `create-mac-app`](#using-the-cli-create-mac-app)\n - [Custom JS/CSS](#custom-jscss)\n   - [Fix links in GMail and Google Calendar](#fix-links-in-gmail-and-google-calendar)\n   - [Reload Slack when it disconnects](#reload-slack-when-it-disconnects)\n   - [Find in page](#find-in-page)\n   - [Drag-and-drop to open URLs](#drag-and-drop-to-open-urls)\n   - [Preview link targets](#preview-link-targets)\n\nI've also written a few blog posts that discuss some of the decisions behind Multi:\n\n - Motivation: \u003chttps://kofi.sexy/blog/multi\u003e\n - Performance: \u003chttps://kofi.sexy/blog/slack-app-fewer-resources\u003e\n - Removing paid licenses: \u003chttps://kofi.sexy/blog/multi-retrospective\u003e\n - 3.0 rewrite: \u003chttps://kofi.sexy/blog/multi-3\u003e\n\nIf you enjoy using Multi, please consider showing your appreciation with a donation!\n\n\u003ca href='https://ko-fi.com/P5P5GCTKI' target='_blank'\u003e\u003cimg height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi3.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /\u003e\u003c/a\u003e\n\n\n## Installation\n\nThe easiest method is to use [Homebrew](https://brew.sh/):\n\n```\nbrew install --cask multi\n```\n\nAlternatively, you can manually download and run the latest `.dmg` from [Releases](https://github.com/kofigumbs/multi/releases).\n\n\n## JSON configuration\n\nMulti apps store their configuration in a single JSON file.\nIf your app is named `Test`, then you'll find that file at `/Applications/Multi/Test.app/Contents/Resources/config.json`.\nThe JSON configuration uses the following top-level fields:\n\n| Field Name                   | Type    | Description                                                                                                                                            |\n|------------------------------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `tabs`                       | Array   | Titles and URLs of tabs for this app ***(Required)***                                                                                                  |\n| `windowed`                   | Boolean | Start this app with each tab in its own window                                                                                                         |\n| `alwaysNotify`               | Boolean | Show macOS notifications even if this app is currently focused                                                                                         |\n| `alwaysOnTop`                | Boolean | Position this app's window on top of all others                                                                                                        |\n| `terminateWithLastWindow`    | Boolean | Determine if this app closes once all tabs/windows are closed                                                                                          |\n| `openNewWindowsInBackground` | Boolean | Determines if browser app becomes active when opening external links                                                                                   |\n| `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` |\n\nThe `tabs` field is an array of objects with the following fields:\n\n| Field Name          | Type             | Description                                                                                                              |\n|---------------------|------------------|--------------------------------------------------------------------------------------------------------------------------|\n| `url`               | String           | Starting page for this tab ***(Required)***                                                                              |\n| `title`             | String           | Name for this tab                                                                                                        |\n| `customJs`          | Array of Strings | Custom JS URLs (see [Custom JS/CSS](#custom-jscss))                                                                      |\n| `customCss`         | Array of Strings | Custom CSS URLs (see [Custom JS/CSS](#custom-jscss))                                                                     |\n| `customCookies`     | Array of Objects | Custom cookies using [HTTPCookiePropertyKey](https://developer.apple.com/documentation/foundation/httpcookiepropertykey) |\n| `basicAuthUser`     | String           | User name credential for requests that use basic access authentication                                                   |\n| `basicAuthPassword` | String           | Password credential for requests that use basic access authentication                                                    |\n| `userAgent`         | String           | Override the default WebKit user agent header                                                                            |\n\nHere's a bare minimum example to recreate the Slack demo video above:\n\n```json\n{ \"tabs\": [{ \"url\": \"https://app.slack.com/client\" }] }\n```\n\nHere's a fancier example that uses the optional fields referenced above:\n\n```json\n{\n  \"tabs\": [\n    {\n      \"title\": \"Dancing\",\n      \"url\": \"https://rc.kofi.sexy/bathroom-floss\",\n      \"basicAuthUser\": \"user\",\n      \"basicAuthPassword\": \"password\",\n      \"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\"\n    },\n    {\n      \"title\": \"Walking\",\n      \"url\": \"https://kofi.sexy/cel-shading\",\n      \"customJs\": [ \"file:///Users/kofi/Documents/dotfiles/main/example.js\" ],\n      \"customCss\": [ \"https://example.com/custom.css\" ],\n      \"customCookies\": [\n        {\n          \"name\": \"login_token_tab\",\n          \"value\": \"eyJoZWxsbyI6ICJ3b3JsZCJ9\",\n          \"domain\": \".example.com\",\n          \"path\": \"/\"\n        }\n      ]\n    }\n  ],\n  \"windowed\": true,\n  \"alwaysNotify\": true,\n  \"alwaysOnTop\": true,\n  \"terminateWithLastWindow\": true,\n  \"openNewWindowsInBackground\": true,\n  \"openNewWindowsWith\": \"com.apple.Safari\"\n}\n```\n\nIf your configuration file fails to decode, you can use the settings window to fix the issues.\nOptional fields will always default to \"empty\" values (i.e. `false`, `\"\"`, `[]`).\n\n\n## Using the CLI: `create-mac-app`\n\nYou can create and update Multi apps entirely from the command-line with the included script.\nIn fact, the Multi configuration UI just runs this script under-the-hood!\nThe `create-mac-app` script takes its options as environment variables.\nFor instance, here's how you'd create a bare-minimum app named `Test`:\n\n```\nMULTI_APP_NAME='Test' /Applications/Multi.app/Contents/Resources/create-mac-app\n```\n\nWhen you open `Test`, you'll be greeted with the preferences window, where you can finish configuring your app.\nIf you'd like to configure your app entirely from the command-line, you can set any of the following variables:\n\n|                     |                                                                |\n|---------------------|----------------------------------------------------------------|\n| `MULTI_ICON_PATH`   | PNG or ICNS path to icon image                                 |\n| `MULTI_JSON_CONFIG` | See [JSON configuration](#json-configuration)                  |\n| `MULTI_OVERWRITE`   | Set to `1` to replace an existing Multi app with the same name |\n\n\n## Custom JS/CSS\n\nMulti lets you customize any site by injecting JavaScript and CSS on every page in your app.\nEach custom JS/CSS file is specified with a URL, which gives you a few options for how you want to manage your customizations:\n\n1. Host your file online, and use its URL: ex. `https://raw.githubusercontent.com/kofigumbs/dotfiles/master/example.js`\n2. Reference a local file on your computer: ex. `file:///Users/kofi/workspace/dotfiles/example.js`\n3. Encode your script directly in the JSON using [Data URIs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs): ex. `data:,console.log%28%27Hello%2C%20from%20Multi%21%27%29%3B%0A`\n\nCustom JS/CSS is one of the most important parts of Multi.\nIt lets the main project stay small and focused, while letting you extend it with new features that fit your use case.\nIf you have a neat JS/CSS snippet, you'd like to share, please open an Issue or Pull Request!\nHere are a few that have come up before:\n\n### Fix links in GMail and Google Calendar\n\nGoogle seems to be doing some trickery here.\nInstead of allowing the browser to handle the \u003ca target=_blank\u003e links, they use JS to open a blank popup window, then dynamically set the URL to google.com/url?q=REAL_URL_HERE.\nPresumably all of this is so that they can track you for a few moments on your way out of their app.\nCustom JS solution:\n\n```js\nwindow.addEventListener(\"DOMContentLoaded\", () =\u003e {\n  const listener = e =\u003e e.stopPropagation();\n  const query = () =\u003e document.querySelectorAll(\"a[target=_blank]\").forEach(a =\u003e {\n    a.removeEventListener(\"click\", listener);\n    a.addEventListener(\"click\", listener, true);\n  });\n  setInterval(query, 400); // wait time between DOM queries, in milliseconds\n});\n```\n\n### Find in page\n\nMulti doesn't include any search functionality (Cmd-F).\nCustom JS solution:\n\n```js\nwindow.addEventListener(\"DOMContentLoaded\", () =\u003e {\n  const highlightResults = (text, color) =\u003e {\n    document.designMode = \"on\"; // https://stackoverflow.com/a/5887719\n    var selection = window.getSelection();\n    selection.collapse(document.body, 0);\n    while (window.find(text)) {\n      document.execCommand(\"HiliteColor\", false, color);\n      selection.collapseToEnd();\n    }\n    document.designMode = \"off\";\n  };\n\n  let mostRecentSearchText = \"\";\n  const search = text =\u003e {\n    highlightResults(mostRecentSearchText, \"transparent\");\n    highlightResults(text, \"rgb(255 255 1 / 50%)\");\n    mostRecentSearchText = text;\n  };\n\n  const input = document.createElement(\"input\");\n  input.placeholder = \"Search...\";\n  input.style.padding = \"10px 15px\";\n  input.style.fontSize = \"15px\";\n  input.style.borderRadius = \"3px\";\n  input.style.border = \"solid 1px lightgray\";\n\n  const form = document.createElement(\"form\");\n  form.style.display = \"none\";\n  form.style.position = \"fixed\";\n  form.style.top = \"15px\";\n  form.style.right = \"15px\";\n  form.style.zIndex = \"2147483647\"; // https://stackoverflow.com/a/856569\n  form.addEventListener(\"submit\", e =\u003e {\n    e.preventDefault();\n    search(input.value);\n  });\n\n  const close = document.createElement(\"a\");\n  close.innerText = \"⨯\";\n  close.href = \"javascript:void(0)\";\n  close.style.fontSize = \"30px\";\n  close.style.padding = \"15px\";\n  close.style.textDecoration = \"none\";\n  close.addEventListener(\"click\", e =\u003e {\n    e.preventDefault();\n    search(\"\");\n    form.style.display = \"none\";\n  });\n\n  form.appendChild(input);\n  form.appendChild(close);\n  document.body.appendChild(form);\n\n  document.addEventListener(\"keydown\", event =\u003e {\n    if (event.metaKey \u0026\u0026 event.key === \"f\") {\n      event.preventDefault();\n      form.style.display = \"block\";\n      input.focus();\n    }\n  });\n});\n```\n\n### Drag-and-drop to open URLs\n\nSay you have a URL outside of Multi (maybe in an email), and you want to open it in Multi.\nCustom JS solution:\n\n```js\ndocument.addEventListener(\"dragover\", e =\u003e e.preventDefault());\n```\n\n### Preview link targets\n\nMulti doesn't include any hover-to-preview-link-target functionality.\nCustom CSS solution:\n\n```css\na:hover::after {\n  content: attr(href);\n  position: fixed;\n  left: 4px;\n  bottom: 4px;\n  padding: 4px;\n  font-size: 12px;\n  font-family: -apple-system, BlinkMacSystemFont;\n  font-weight: normal;\n  color: black;\n  background: ghostwhite;\n  border: solid 1px black;\n  border-radius: 1px;\n}\n```\n","funding_links":["https://ko-fi.com/P5P5GCTKI'"],"categories":["Swift","cli"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkofigumbs%2Fmulti","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkofigumbs%2Fmulti","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkofigumbs%2Fmulti/lists"}