Bike
Bike 2 (Preview)
Bike 2 (Preview)
  • Bike 2 (Preview 212)
  • Using Bike 2
    • Outline Editing
    • Using Selection
    • Using Themes
    • Using Outline Paths
    • Using Outline Filtering
  • Customizing Bike
    • Creating Extensions
    • Creating View Extensions
    • Creating Themes
  • Known Bugs
Powered by GitBook
On this page
  • Overview
  • Extensions Folder
  • Quick Tour
  • Setup Environment
  • Understand Environment
  • Create a new Command
  • Next Steps
  1. Customizing Bike

Creating Extensions

Last updated 22 days ago

Extensions allow you to customize and enhance the functionality of Bike.

Overview

Extensions are written in TypeScript. They can add new commands, keybindings, views, and other features to Bike. Sensitive features are protected by a permission system.

Extensions Folder

Extensions are loaded from Bike's extensions folder. Use the menu Bike > Extensions… to open that folder in the Finder.

Inside, you will find two folders:

  • @Bike: This extension ships with Bike and includes API documentation. It is replaced each time Bike launches. This is extension is always loaded first and is a good place to see how Bike APIs work.

  • @Startup: This extension is always loaded second and will be recreated if deleted. Changes you make are preserved. It's a good place to experiment.

Quick Tour

Open the entire Extensions folder in an editor with good TypeScript support. and work well. The folder structure looks like this:

Extensions/
  @Bike/
    @configs/
    @types/
    src/
  @Startup/
    manifest.json
    src/
      dom/
      main.ts

Each extension has two key files:

./manifest.json

This file configures your extension:

  • version: The extension version.

  • api_version: The version of Bike's API that this extension requires. This is not the same as Bike's version number.

  • safari_inspectable: If true (and Safari is set up for debugging) Bike will launch a JavaScript inspector in Safari when it starts.

  • permissions: An array defining what the extension is allowed to do. By default, the startup extension has permission to read and write to the clipboard.

./src/main.ts

This is the entry point for your extension. It should export an activate function, which is called when the extension is loaded.

export function activate(context: ExtensionContext) {
  ...
}

In the @Bike extension, you can see examples of how commands, keybindings, and sidebar items are added to Bike. Right-click on any symbol and then choose "Go to Definition" from the popup to see API comments and options.

Many APIs follow a similar pattern: when you add something (like a command), a Disposable is returned. This disposable acts as a handle to the addition. To remove it, call .dispose().

Bike automatically disposes of everything added by your extension when it reloads. You only need to keep track of disposables if you plan to update or remove items dynamically.

Setup Environment

Extensions involve programming, and having the right environment makes it easier.

  1. Open the @Startup extension in an editor with TypeScript support. This provides autocomplete and error-checking.

  2. Open Bike's Window > Logs Explorer to see Bike's logs. Errors and your own calls to console.log will show up here.

  3. Enable Safari debugging in manifest.json and in Safari:

    • Go to Safari > Settings > Advanced and check "Show features for web developers".

    • Choose menu item Safari > Develop > Inspect Apps and Devices to show a window with all inspectable script contexts.

    • Start Bike 2 and it should show in the Safari window from previous step. You may want to click the ... to the right of Bike's icon and check "Automatically Inspect New JSContexts". When you launch Bike, Safari should open a web inspector, allowing you to debug your extension.

Understand Environment

Each extension runs in its own isolated JSContext.

Create a new Command

Let's modify the @Startup extension to define a new "Archive Done" command. The command will move all completed items to an "Archive" row. We'll be editing the main.ts file.

First add a function that will implement the archive done command:

function archiveDoneCommand(): boolean {
  console.log("Archive Done!");
  return true;
}

So far we've just added a function, next let's associate that function with a Bike Command:

export function activate(context: ExtensionContext) {
  bike.commands.addCommands({
    commands: {
      "startup:archive-done": archiveDoneCommand,
    },
  });
}

After saving your changes, Bike will automatically reload extensions, and the command will be available in the Command Pallet (Command-Shift-P). Try it out! When you select the command you should see "Archive Done!" printed in Bike's Logs Explorer window.

Next lets add a keybinding to activate this command. This will allow us to activate the command without having to use the Command Pallet:

export function activate(context: ExtensionContext) {
  ...
  bike.keybindings.addKeybindings({
    keymap: "block-mode",
    keybindings: {
      "a": "startup:archive-done",
    },
  });
}

After saving, enter block mode (press Escape), then type a. You should see "Archive Done!" printed in the Logs Explorer window. Keybindings are used when Bike's outline editor has keyboard focus. They are not used when other UI elements have keyboard focus.

Let's implement the archive done command for real:

  1. Find checked-off rows.

  2. Locate (or create) the Archive row.

  3. Move completed items into the Archive row.

import { Row } from "outline";

...

function archiveDoneCommand(): boolean {
  // Get frontmost editor
  let editor = bike.frontmostOutlineEditor
  if (!editor) return false

  // Get the outline, done rows, and archive row
  let outline = editor.outline
  let donePath = "//@data-done except //@id = archive//*"
  let doneRows = outline.query(donePath).value as Row[]
  let archiveRow = (outline.query("//@id = archive").value as Row[])[0]
  
  // Insert an Archive row if needed and move done rows
  outline.transaction({ animate: "default" }, () => {
    if (!archiveRow) {
      archiveRow = outline.insertRows([{ 
        id: "archive",
        text: "Archive",
      }], outline.root)[0]
    }
    outline.moveRows(doneRows, archiveRow)
  })
    
  return true
}

Now, when you run the Archive Done command, completed rows will be moved into the Archive section.

Next Steps

host_permissions: An array of that this extension is allowed to access using fetch. Fetching requires both the "fetch" permission and that fetched URLs match at least one of the patterns here.

Bike includes an embedded version of the build tool. This is used to bundle your extension code into a single script so that it can be loaded into the JSContext. This step is required because JSContext doesn't support Typescript or import statements.

Experiment with modifying @Startup, exploring the API, and building your own commands. To display custom UI see .

VS Code
Zed
URL patterns
esbuild
Creating View Extensions