Bike 2 Extensions Preview

This wasn’t really part of the plan, but I’m excited about it…

My logic:

  1. Bike 2 will support search/filter of outlines
  2. Search/filter is more compelling with a sidebar of possible searches
  3. Sidebar is more compelling if you can add your own items to it.
  4. I’ve always wanted to build a full extension system…

The goal for Friday is to have a system you can play with. The goal for Bike 2 is to build out a useful API and figure technical of extension system … but skip extension management UI (enable, disable, update, etc). For now I expect usage to be mainly through a single startup extension (like a startup script that’s run once every time you start Bike).

After this Friday release I will stop expanding the 2.0 feature set and start trying to make it all actually work.

9 Likes

Looks great ! :slight_smile:

1 Like

I’m not sure this is the best place for this question, but do you think that Extensions would also enable multi-pane views or is that an entirely different beast?

No, yes, maybe…

No, generally the entire extensions API is mostly a surface for automating things that I’ve already built in Swift. So extensions can’t really create new things, instead they automate existing things and also (the big feature beyond scripts) react to changes and show up in the UI in designated places like sidebar.

Yes, the feature of multiple panes (Split View) is something that I’ve sketched out/even mostly built a number of times. I want it. But it hasn’t quite reached priority. I “think” the way I will eventually do it is have a fairly limited implementation where you have the option for one editor view, or option to split that view into two views. As I’ve been trying to define the extension API over the last week I’ve tried to look ahead and make the APIs compatible with future designs like this. So for example Window has an associated array of OutlineEditors, not just a single one. When splits like this actually get implemented I don’t know, but not for 2.0 initial release.

Maybe, one extension feature that I very much do want to have is the ability for extensions to display arbitrary UI in a web view. The idea is the extension can (for example) pick out rows using an outline search path, send those results to web view, and then display as desired. I expect to have an inspector sidebar dedicated for presenting these extension views. Not sure if this will make 2.0 initial release either, but I expect it to make into app before editor split views.

This is looking amazing…

2 Likes

This is looking great! I love the concept of being able to add automations like that. And JS is pretty straightforward.

And not going to make that goal :frowning:

I did make a lot of good progress on extensions this week!

Two big additions:

First an API for extensions to register commands. Commands are basically an id, name, and callback. When the command is invoked the callback is called. Commands will be available in a global command palette.

Second an API to register keybindings. Keybindings allow you to register a key sequence with a command. Bindings are registered to a specific context (text or block) and take priority over system bindings.

Commands paired with keybindings make block mode more useful and interesting. Assign any key sequence that you want to run custom logic.

The last big change that I need to make to extensions is merge in the theme system. It’s all the same “run javascript as plugin” technology, but they evolved as completely different systems and now I think it makes sense to merge them.

A list of extension system plans for the Bike 2.0 release. Need to set some limits so I can finish! Maybe if I write them down it will help?

Extension system will…

  • Be in “preview”. I think it needs people playing with it for a while before calling it 1.0 ready.

  • Not support any polished path for sharing or updating extensions. For 2.0 I expect people will mostly be testing the API’s and creating their own custom extensions.

  • Not support any additional big parts such as web views, background workers, etc. They will hopefully come later.

  • Will not define a standard set of block mode keybindings… though this is something that I very much want to do eventually. I think a whole VIM inspired mini language would be great for block mode, but need time to experiment before designing that.

  • Time for bed!

5 Likes

Is this the best place to ask questions about the extensions API ?

A first question might be about keybindings:

In the 2.0 (215) @Startup main.ts, I find that I can change things successfully by re-saving. (Adjusting, for example, the text of the sidebar Home item), but I’m not sure whether this:

  bike.keybindings.addKeybindings({
    keymap: "block-mode",
    source: "startup",
    keybindings: {
      "m d": "startup:toggle-done",
    },
  });

suggests that I should already be finding that Cmd-d toggles the done state of selected lines ?

Yes, anywhere is good :slight_smile:

No, m d means type m and then type d (before the 1 second reset timeout). So it’s a two character sequence without any modifier keys.

For Command-D you would use either cmd-d or command-d… except when I just tested that I noticed that it doesn’t work quite right. In the current preview commands with modifier key will get called twice… and in that case of toggle done that means it will appear like nothing has happened.

I’ll make a new release tomorrow that fixes.

1 Like

Got it !

(and mea culpa – I should have spotted that in the @Bike.keybindings.d.ts file)

m, and then an immediate d with a row selected in Block mode do work here in 215 – toggling the done status.

The first keystroke harvests a (loudish :-)) system ‘Tink’ sound, which FWIW I initially heard as a signal that the keys were unbound, or not working. My misunderstanding.

( JSContext visibility in the Safari Develop menu, and the ability to experiment at the Safari JS Console, is very helpful, incidentally )

Any idea why it does that, or is anyone else hearing that? It don’t hear anything from my computer.

Not sure, it’s a fresh account (macOS 15.4) so settings should be default, I think.

When we switch to Block mode (Bike 2 (215)) and select a row, hitting any printing key (Alphanumerics, punctuation – with or without modifiers, most function keys but not F5, F11) triggers a Tink sound.

No Tink from Cursor keys, esc, or tab, or from modifier keys on their own.

However, where the Bike 2 keybindings define two consecutive Block mode keys (like "m d" for toggle done), only the first key gets a Tink. The second key is silent, if part of the key binding.

(If we follow the bound “m” with some other key, which doesn’t feature in any binding, then that second keystroke also gets a Tink)

FWIW in the console there appear to be a sequence of systemsoundserver requests related to Bike, if I tap any unbound key (or the initial m, of "m d") repeatedly on a Block view row.

Click triangle to view log
default	12:50:23.971179+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 14, inClientAudioSessionID 0
default	12:50:24.365602+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 15, inClientAudioSessionID 0
default	12:50:24.368463+0100	systemsoundserverd	      ActiveSoundList.cpp:78    token 115, mActionID 4096, process 535(Bike), mClientConnection -33340048, mPlayerSynchronizer 0x0, mPlayFlags->ShouldPlayAudio 1, mPlayFlags->ShouldVibe 0, mAudioFinishedPlaying 0, mVibeFinishedPlaying 0, mIsLoopedSound 0, mClientCompletionToken 15, mReporterID 0, timeSinceClientRequestedStopInMS 0, elapsedTimeInMS 1
default	12:50:25.228295+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 16, inClientAudioSessionID 0
default	12:50:25.557322+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 17, inClientAudioSessionID 0
default	12:50:25.560290+0100	systemsoundserverd	      ActiveSoundList.cpp:78    token 117, mActionID 4096, process 535(Bike), mClientConnection -33340048, mPlayerSynchronizer 0x0, mPlayFlags->ShouldPlayAudio 1, mPlayFlags->ShouldVibe 0, mAudioFinishedPlaying 0, mVibeFinishedPlaying 0, mIsLoopedSound 0, mClientCompletionToken 17, mReporterID 0, timeSinceClientRequestedStopInMS 0, elapsedTimeInMS 2
default	12:50:26.234112+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 18, inClientAudioSessionID 0
default	12:50:26.427221+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 19, inClientAudioSessionID 0
default	12:50:26.430371+0100	systemsoundserverd	      ActiveSoundList.cpp:78    token 119, mActionID 4096, process 535(Bike), mClientConnection -33340048, mPlayerSynchronizer 0x0, mPlayFlags->ShouldPlayAudio 1, mPlayFlags->ShouldVibe 0, mAudioFinishedPlaying 0, mVibeFinishedPlaying 0, mIsLoopedSound 0, mClientCompletionToken 19, mReporterID 0, timeSinceClientRequestedStopInMS 0, elapsedTimeInMS 2
default	12:50:26.592001+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 20, inClientAudioSessionID 0
default	12:50:26.594254+0100	systemsoundserverd	      ActiveSoundList.cpp:78    token 120, mActionID 4096, process 535(Bike), mClientConnection -33340048, mPlayerSynchronizer 0x0, mPlayFlags->ShouldPlayAudio 1, mPlayFlags->ShouldVibe 0, mAudioFinishedPlaying 0, mVibeFinishedPlaying 0, mIsLoopedSound 0, mClientCompletionToken 20, mReporterID 0, timeSinceClientRequestedStopInMS 0, elapsedTimeInMS 1
default	12:50:26.779706+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 21, inClientAudioSessionID 0
default	12:50:26.782246+0100	systemsoundserverd	      ActiveSoundList.cpp:78    token 121, mActionID 4096, process 535(Bike), mClientConnection -33340048, mPlayerSynchronizer 0x0, mPlayFlags->ShouldPlayAudio 1, mPlayFlags->ShouldVibe 0, mAudioFinishedPlaying 0, mVibeFinishedPlaying 0, mIsLoopedSound 0, mClientCompletionToken 21, mReporterID 0, timeSinceClientRequestedStopInMS 0, elapsedTimeInMS 1
default	12:50:26.938698+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 22, inClientAudioSessionID 0
default	12:50:26.941764+0100	systemsoundserverd	      ActiveSoundList.cpp:78    token 122, mActionID 4096, process 535(Bike), mClientConnection -33340048, mPlayerSynchronizer 0x0, mPlayFlags->ShouldPlayAudio 1, mPlayFlags->ShouldVibe 0, mAudioFinishedPlaying 0, mVibeFinishedPlaying 0, mIsLoopedSound 0, mClientCompletionToken 22, mReporterID 0, timeSinceClientRequestedStopInMS 0, elapsedTimeInMS 2
default	12:50:27.091662+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 23, inClientAudioSessionID 0
default	12:50:27.094140+0100	systemsoundserverd	      ActiveSoundList.cpp:78    token 123, mActionID 4096, process 535(Bike), mClientConnection -33340048, mPlayerSynchronizer 0x0, mPlayFlags->ShouldPlayAudio 1, mPlayFlags->ShouldVibe 0, mAudioFinishedPlaying 0, mVibeFinishedPlaying 0, mIsLoopedSound 0, mClientCompletionToken 23, mReporterID 0, timeSinceClientRequestedStopInMS 0, elapsedTimeInMS 1
default	12:50:27.232708+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 24, inClientAudioSessionID 0
default	12:50:27.235172+0100	systemsoundserverd	      ActiveSoundList.cpp:78    token 124, mActionID 4096, process 535(Bike), mClientConnection -33340048, mPlayerSynchronizer 0x0, mPlayFlags->ShouldPlayAudio 1, mPlayFlags->ShouldVibe 0, mAudioFinishedPlaying 0, mVibeFinishedPlaying 0, mIsLoopedSound 0, mClientCompletionToken 24, mReporterID 0, timeSinceClientRequestedStopInMS 0, elapsedTimeInMS 1
default	12:50:27.429085+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 25, inClientAudioSessionID 0
default	12:50:27.432554+0100	systemsoundserverd	      ActiveSoundList.cpp:78    token 125, mActionID 4096, process 535(Bike), mClientConnection -33340048, mPlayerSynchronizer 0x0, mPlayFlags->ShouldPlayAudio 1, mPlayFlags->ShouldVibe 0, mAudioFinishedPlaying 0, mVibeFinishedPlaying 0, mIsLoopedSound 0, mClientCompletionToken 25, mReporterID 0, timeSinceClientRequestedStopInMS 0, elapsedTimeInMS 2
default	12:50:27.578649+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 26, inClientAudioSessionID 0
default	12:50:27.580724+0100	systemsoundserverd	      ActiveSoundList.cpp:78    token 126, mActionID 4096, process 535(Bike), mClientConnection -33340048, mPlayerSynchronizer 0x0, mPlayFlags->ShouldPlayAudio 1, mPlayFlags->ShouldVibe 0, mAudioFinishedPlaying 0, mVibeFinishedPlaying 0, mIsLoopedSound 0, mClientCompletionToken 26, mReporterID 0, timeSinceClientRequestedStopInMS 0, elapsedTimeInMS 1
default	12:50:27.752341+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 27, inClientAudioSessionID 0
default	12:50:27.755422+0100	systemsoundserverd	      ActiveSoundList.cpp:78    token 127, mActionID 4096, process 535(Bike), mClientConnection -33340048, mPlayerSynchronizer 0x0, mPlayFlags->ShouldPlayAudio 1, mPlayFlags->ShouldVibe 0, mAudioFinishedPlaying 0, mVibeFinishedPlaying 0, mIsLoopedSound 0, mClientCompletionToken 27, mReporterID 0, timeSinceClientRequestedStopInMS 0, elapsedTimeInMS 2
default	12:50:27.933939+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 28, inClientAudioSessionID 0
default	12:50:27.936321+0100	systemsoundserverd	      ActiveSoundList.cpp:78    token 128, mActionID 4096, process 535(Bike), mClientConnection -33340048, mPlayerSynchronizer 0x0, mPlayFlags->ShouldPlayAudio 1, mPlayFlags->ShouldVibe 0, mAudioFinishedPlaying 0, mVibeFinishedPlaying 0, mIsLoopedSound 0, mClientCompletionToken 28, mReporterID 0, timeSinceClientRequestedStopInMS 0, elapsedTimeInMS 1
default	12:50:28.119144+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 29, inClientAudioSessionID 0
default	12:50:28.121614+0100	systemsoundserverd	      ActiveSoundList.cpp:78    token 129, mActionID 4096, process 535(Bike), mClientConnection -33340048, mPlayerSynchronizer 0x0, mPlayFlags->ShouldPlayAudio 1, mPlayFlags->ShouldVibe 0, mAudioFinishedPlaying 0, mVibeFinishedPlaying 0, mIsLoopedSound 0, mClientCompletionToken 29, mReporterID 0, timeSinceClientRequestedStopInMS 0, elapsedTimeInMS 1
default	12:50:28.270137+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 30, inClientAudioSessionID 0
default	12:50:28.273240+0100	systemsoundserverd	      ActiveSoundList.cpp:78    token 130, mActionID 4096, process 535(Bike), mClientConnection -33340048, mPlayerSynchronizer 0x0, mPlayFlags->ShouldPlayAudio 1, mPlayFlags->ShouldVibe 0, mAudioFinishedPlaying 0, mVibeFinishedPlaying 0, mIsLoopedSound 0, mClientCompletionToken 30, mReporterID 0, timeSinceClientRequestedStopInMS 0, elapsedTimeInMS 2
default	12:50:28.465214+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 31, inClientAudioSessionID 0
default	12:50:28.467583+0100	systemsoundserverd	      ActiveSoundList.cpp:78    token 131, mActionID 4096, process 535(Bike), mClientConnection -33340048, mPlayerSynchronizer 0x0, mPlayFlags->ShouldPlayAudio 1, mPlayFlags->ShouldVibe 0, mAudioFinishedPlaying 0, mVibeFinishedPlaying 0, mIsLoopedSound 0, mClientCompletionToken 31, mReporterID 0, timeSinceClientRequestedStopInMS 0, elapsedTimeInMS 1
default	12:50:28.636615+0100	systemsoundserverd	          SSServerImp.cpp:733   -> Incoming Request : actionID 4096, inClientPID 535(Bike), inBehavior 1, customVibeDataProvided 0, loop 0, loopPeriod 0.000000, inFlags 0, prefersToDisallowExternalPlayback: 0, inClientCompletionToken 32, inClientAudioSessionID 0
default	12:50:28.639518+0100	systemsoundserverd	      ActiveSoundList.cpp:78    token 132, mActionID 4096, process 535(Bike), mClientConnection -33340048, mPlayerSynchronizer 0x0, mPlayFlags->ShouldPlayAudio 1, mPlayFlags->ShouldVibe 0, mAudioFinishedPlaying 0, mVibeFinishedPlaying 0, mIsLoopedSound 0, mClientCompletionToken 32, mReporterID 0, timeSinceClientRequestedStopInMS 0, elapsedTimeInMS 1

Thanks, I guess there must be some way that I can control it then, I’ll investigate

Edit Turns out my volume for those alerts was too low and I just couldn’t hear. Oops.

1 Like

Keybinding sounds all fixed in 216 :pray:

1 Like

The Bike 2.0 (preview) JSContext scripting is very fast !

Thinking about its interaction with the rest of the system, and for example, reconstructing a slightly specialised Bike 1.0 osascript Copy As like this:

(which prepares an html rendering of selected rows, in Mail’s native format, and places them in a public.html pasteboard item with a JXA function like the one below)

Expand disclosure triangle to view JS source
// setClipOfTextType :: String -> String -> IO String
const setClipOfTextType = utiOrBundleID =>
    txt => {
        const pb = $.NSPasteboard.generalPasteboard;

        return (
            pb.clearContents,
            pb.setStringForType(
                $(txt),
                utiOrBundleID
            ),
            txt
        );
    };

I wonder whether, at some point, it might look feasible to enrich the Bike 2.0 Clipboard interface, adding uti or bundleID parameters to writeText and readText, or perhaps to generalised siblings of them ?

(as in the .setStringForType and .dataForType of NSPasteboard's generalPasteboard)


( In the meanwhile of course, an external process can already read and parse the com.hogbaysoftware.bike.xml of Bike 2.0’s clipboard )

1 Like

Good idea, I’ll add for next release.

1 Like

Is there any way, in the extensions api, of obtaining a bike xml (or other) serialization of some subset of the rows in an outline ?

(e.g. those selected, or matching a path)

Yes, it’s not super obvious, but here’s what you need:

let outline = editor.outline
let filteredOutline = new Outline(outline.query("//not @done"))
let archived = filteredOutline.archive()        

For more info look in the docs for RowSource. Row source can be lots of different things (one of which is query result), and can be used to either initialize a new outline or insert rows into an existing outline.

1 Like