Skip to content

ValentinWalter/Honey

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

38 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🍯 Honey

A Swift API for interacting with 🐻 Bear. If it's on Bear's documentation, Honey can do it.

Honey is based on Middleman, a completely type-safe way of handling the x-callback-url scheme. x-callback-url can be a fickle thing to work with: type-safety can go a long way in helping you work with this. Auto-complete will enable you to discover the API as you work with it. Honey also handles repetitive tasks, like passing Bear's API token where it's required or base64-encoding images and files.

β›° Overview

Action Implemented as
/open-note open(note:)
/create create(note:)
/add-text add(text:)
/add-file add(file:)
/tags allTags()
/open-tag open(tag:)
/rename-tag rename(tag:)
/delete-tag delete(tag:)
/trash trash(id:)
/archive archive(id:)
/untagged allUntagged() searchUntagged()
/todo allTodos() searchTodos()
/today allToday() searchToday()
/locked searchLocked()
/search search(for:)
/grab-url create(from:)
/change-theme change(theme:)
/change-fontΒ  change(font:)

Extra goodies

  • read(note:) Returns the content of a note without opening it.
  • open(tab:) Opens one of Bear's tabs (Untagged/Locked/Trash, etc.) or any of your tags.
  • pin(note:) Pins a note.

Next steps

  • Implement a command-line interface using apple/swift-argument-parser
  • Refactor. Right now the API is relatively close to Bear's documentation. For example, functions like the various searchX actions could be consolidated into one.
  • Migrate from callbacks to async in Swift 6.
  • Add tests

Example

Let's create a shopping list.

let note = Note(
    title: "πŸ› Shopping list",
    body: """
    - 🍎 Apples
    - πŸ₯£ Cereal	
    """
}
	
Bear.create(note, options: .pin) { shoppingList in
    // We forgot cheese!
    Bear.add(
        text: "- πŸ§€ Cheese",
        to: .id(shoppingList.id),
        mode: .append
    )
}

πŸ›  Setup

Installation

Honey is a Swift Package. Install it by pasting this in your Package.swift:

let package = Package(
    ...
    dependencies: [
        .package(url: "https://github.com/ValentinWalter/honey.git", from: "1.0.0")
    ],
    ...
)

Configuration

Provide your API token if you plan on using any actions that require an API token. You do this directly by setting Bear.token. Or preferably with an environment variable called BEAR_API_TOKEN. In Xcode:

Edit scheme… > Run > Arguments > Environment Variables

Receiving urls

As Honey is based on Middleman, you will need to configure Middleman as well. To receive callbacks you need to make sure your app has a custom url scheme implemented. Middleman will then automatically read the first entry in the CFBundleURLTypes array in the main bundle's Info.plist.

For Middleman to be able to parse incoming urls, you need to put one of the following methods in the delegate (UIKit/Cocoa) appropriate for your platform or in the onOpenURL SwiftUI modifier.

// SwiftUI
// On any view (maybe in your `App`)
.onOpenURL { url in
    Middleman.receive(url)
}

// macOS
// In your `NSAppDelegate`:
func application(_ application: NSApplication, open urls: [URL]) {
    Middleman.receive(urls)
}

// iOS 13 and up
// In your `UISceneDelegate`:
func scene(_ scene: UIScene, openURLContexts urlContexts: Set<UIOpenURLContext>) {
    Middleman.receive(urlContexts)
}

// iOS 12 and below
// In your `UIAppDelegate`:
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    Middleman.receive(url)
}

πŸ‘Ύ API

All actions and types Honey implements are namespaced under Bear. The general workflow is to type Bear. and choose your desired action from the auto-complete menu. All actions follow the same kind of structure:

// The full signature of most actions
Bear.action(parameter: value, ...) { output in
    ...
} onError: {
    ...
}

// Trailing closure syntax of most actions
// Unlabeled trailing closures always mean success
Bear.action(parameter: value, ...) { output in
    ...
}

// No closures syntax of most actions
// Most action's paramaters are optional, same with callbacks
Bear.action(parameter: value, ...)

πŸ’₯ Actions

Here are some cool examples. You can find a full list of actions in the overview.

Open

Bear.open(note: .id("9ASG...JA2FJ", at: "Header")

Bear.read(note: .title("πŸ› Shopping list") { note in
    print(note.body)
    print(note.id)
}

Create

let note = Bear.Note(
    title: "Title",
    body: "body",
    tags: ["Tag"],
    isPinned: false
)

Bear.create(note) { note in
    print(note.id)
}

Add Text

Bear.add(
    text: "\(Date())",
    to: .selected,
    mode: .append
)

Add File

let url = URL(string: "https://apod.nasa.gov/apod/image/2105/M8_rim2geminicrop600.jpg")!
let data = Data(contentsOf: url)!
let image = Bear.File(name: "The Southern Cliff in the Lagoon", data: data)

Bear.add(
    file: image,
    to: .title("πŸͺ Daily astronomy pictures"),
    at: "Sat May 15",
    mode: .prepend
)

Search

Bear.search(
    for: "important notes",
    in: "some tag"
) { notes in
    print(notes.map(\.title))
}

Change Theme & Font

Bear.change(theme: .oliveDunk)
Bear.change(font: .avenirNext)

πŸ¦‹ Types

Honey implements various concepts of Bear's API as types in Swift.

Note

A Note is used to create and read notes.

// Create notes for the create(_:) action
let note = Bear.Note(
    title: "A Title",
    body: "A paragraph...",
    tags: ["A", "few", "tags"],
    isPinned: false
)

// Or when you received a note via the output of an action
note.modificationDate
note.creationDate
note.id

Note.Lookup

You use this enum to find already existing notes.

case title(String)
case id(String)
case selected // requires an API token

// Use like this
Bear.open(note: .title("πŸ› Shopping list"))
Bear.read(note: .id("9ASG...JA2FJ"))
Bear.add(text: "...", note: .selected)

You can get extra fancy by using Note.Lookup as namespace for notes you access often.

extension Bear.Note.Lookup {
    static let home: Self = .title("🏑 Home")
    
    // Or better yet, use an ID namespaced in Bear
    static let journal: Self = .id(Bear.journalID)
}

extension Bear {
    static let journalID = "E3F...2A8"
}

// You can now do this πŸ₯³
Bear.open(note: .home)
Bear.read(note: .journal)

Options

The Options type is an OptionSet. This means you can mix and match any of the options.

// Don't show the note.
static let hideNote
// Open a new window.
static let newWindow
// Make the new window float (contingent on `newWindow`).
static let float
// Don't show Bear's window.
static let hideWindow
// Exclude notes in the trash from the action.
static let excludeTrashed
// Pin the note.
static let pin
// Place a cursor inside the note.
static let edit
// Append the current date and time at the end of the note.
static let timestamp

// Use like this
Bear.open(..., options: .edit)
Bear.create(..., options: [.pin, .newWindow])

Tag

Tag behaves the same way a usual String does. Similar to Note.Lookup you can use this type to namespace your frequently used tags.

extension Tag {
    static let work: Self = "πŸ‘Ύ Work"
}

File

When dealing with files in either create(_:) or add(file:), the File type comes in handy.

let url = URL(string: "https://apod.nasa.gov/apod/image/2105/M8_rim2geminicrop600.jpg")!
let data = Data(contentsOf: url)!

let file = Bear.File(
    name: "The Southern Cliff in the Lagoon", 
    data: data
)

Tab, Theme, Font

// Tab lets you open tabs or tags from Bear's sidebar via open(tab:)
case all
case untagged
case ...
case trash
case tag(String)

// Theme lets you change themes via change(theme:)
case redGraphite
case ...
case lighthouse

// Font lets you change fonts via change(font:)
case avenirNext
case ...    
case openDyslexic