├── CHANGELOG.md ├── .ruby-version ├── .gitignore ├── .prettierrc ├── .onsaverc ├── assets ├── appicon.icns ├── appicon.png ├── appicon.pxm └── Bearing.iconsproj ├── Gemfile ├── Gemfile.lock ├── src ├── main.rb ├── cli.rb ├── bearing │ ├── incoming-uri-call.rb │ ├── shell-integration.rb │ ├── routing.rb │ ├── menu.rb │ ├── bear.rb │ ├── ipc.rb │ └── cmd.rb └── config.rb ├── LICENSE.md └── README.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.6 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/ 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | addTrailingCommas: true 2 | -------------------------------------------------------------------------------- /.onsaverc: -------------------------------------------------------------------------------- 1 | { 2 | "**/*.{rake,rb}": ["chruby-exec 2.6.5 -- rbprettier --write"] 3 | } 4 | -------------------------------------------------------------------------------- /assets/appicon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/czottmann/bearing/HEAD/assets/appicon.icns -------------------------------------------------------------------------------- /assets/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/czottmann/bearing/HEAD/assets/appicon.png -------------------------------------------------------------------------------- /assets/appicon.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/czottmann/bearing/HEAD/assets/appicon.pxm -------------------------------------------------------------------------------- /assets/Bearing.iconsproj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/czottmann/bearing/HEAD/assets/Bearing.iconsproj -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | group :development do 4 | gem 'prettier', '~> 0.18.2' 5 | end 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | prettier (0.18.2) 5 | 6 | PLATFORMS 7 | ruby 8 | 9 | DEPENDENCIES 10 | prettier (~> 0.18.2) 11 | 12 | BUNDLED WITH 13 | 2.1.4 14 | -------------------------------------------------------------------------------- /src/main.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require_relative 'src/config' 4 | require_relative 'src/bearing/menu' 5 | require_relative 'src/bearing/routing' 6 | 7 | ARGV[0] ? Bearing::Routing.handle(ARGV[0]) : Bearing::Menu.print_items 8 | -------------------------------------------------------------------------------- /src/cli.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require_relative 'config' 4 | require_relative 'bearing/bear' 5 | require_relative 'bearing/cmd' 6 | require_relative 'bearing/ipc' 7 | 8 | action, *args = ARGV 9 | ipc = Bearing::IPC.new 10 | 11 | Bearing::Cmd.validate_action_argument!(action) 12 | Bearing::Cmd.start_app 13 | Bearing::Bear.call_api(action: action, args: args, call_id: ipc.call_id) 14 | 15 | puts ipc.wait_for_incoming_data 16 | -------------------------------------------------------------------------------- /src/bearing/incoming-uri-call.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'uri' 4 | 5 | require_relative 'ipc' 6 | 7 | module Bearing 8 | module IncomingURICall 9 | class << self 10 | def handle(uri_string = '') 11 | call_id = URI.parse(uri_string).host 12 | ipc = Bearing::IPC.new(call_id: call_id) 13 | ipc.write_incoming_data_to_tmp_file(uri_string) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /src/bearing/shell-integration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'fileutils' 4 | 5 | require_relative '../config' 6 | 7 | module Bearing 8 | module ShellIntegration 9 | class << self 10 | def install 11 | orig = File.expand_path(File.dirname(__FILE__) + '/../cli.rb') 12 | FileUtils.ln_sf(orig, ::SHELL_INTEGRATION_PATH) 13 | end 14 | 15 | def uninstall 16 | FileUtils.remove(::SHELL_INTEGRATION_PATH) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /src/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | APP_BUNDLE_ID = 'org.zottmann.Bearing' 4 | AVAILABLE_BEAR_ACTIONS = %w[ 5 | open-note 6 | create 7 | add-text 8 | add-file 9 | tags 10 | open-tag 11 | rename-tag 12 | delete-tag 13 | trash 14 | archive 15 | untagged 16 | todo 17 | today 18 | locked 19 | search 20 | grab-url 21 | change-theme 22 | change-font 23 | ] 24 | SHELL_INTEGRATION_PATH = '/usr/local/bin/bearing' 25 | URI_SCHEME = 'bearing' 26 | VERSION = '2020.06.19' 27 | -------------------------------------------------------------------------------- /src/bearing/routing.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'uri' 4 | 5 | require_relative '../config' 6 | require_relative 'incoming-uri-call' 7 | require_relative 'shell-integration' 8 | 9 | module Bearing 10 | module Routing 11 | class << self 12 | def handle(arg = '') 13 | if arg.start_with?(::URI_SCHEME + '://') 14 | Bearing::IncomingURICall.handle(arg) 15 | elsif arg.start_with?('Install') 16 | Bearing::ShellIntegration.install 17 | elsif arg.start_with?('Uninstall') 18 | Bearing::ShellIntegration.uninstall 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /src/bearing/menu.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../config' 4 | 5 | module Bearing 6 | module Menu 7 | class << self 8 | def print_items 9 | verb = File.exist?(::SHELL_INTEGRATION_PATH) ? 'Uninstall' : 'Install' 10 | menu_entry = "#{verb} #{::SHELL_INTEGRATION_PATH}" 11 | 12 | puts [ 13 | 'DISABLED|Bearing, a scripting helper for Bear.', 14 | "DISABLED|Version #{::VERSION}", 15 | '----', 16 | menu_entry, 17 | '----', 18 | 'DISABLED|Made with ❤️ in Munich in 2020', 19 | 'DISABLED|by Carlo Zottmann ', 20 | "DISABLED|Let's be excellent to eachother :)", 21 | ].join("\n") 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /src/bearing/bear.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'erb' 4 | 5 | require_relative '../config' 6 | 7 | module Bearing 8 | module Bear 9 | class << self 10 | include ERB::Util 11 | 12 | def call_api(action: '', args: [], call_id: '') 13 | response_base = "#{::URI_SCHEME}://#{call_id}" 14 | query = 15 | [ 16 | args_to_url_query(args), 17 | 'x-error=' + url_encode(response_base + '/error'), 18 | 'x-success=' + url_encode(response_base + '/success'), 19 | ].join('&') 20 | 21 | system("open 'bear://x-callback-url/#{action}?#{query}'") 22 | end 23 | 24 | private 25 | 26 | def args_to_url_query(args = []) 27 | # Split `--x=y` CLI arguments into KV pairs 28 | pairs = Hash[args.flat_map { |s| s.scan(/^--([a-z_]+)=(.*)$/) }] 29 | 30 | # Turn KV pairs into query parameters 31 | pairs.map do |k, v| 32 | next unless k && v 33 | url_encode(k) + '=' + url_encode(v) 34 | end.compact.join('&') 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020 Carlo Zottmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/bearing/ipc.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cgi' 4 | require 'fileutils' 5 | require 'json' 6 | require 'securerandom' 7 | require 'timeout' 8 | require 'uri' 9 | 10 | require_relative '../config' 11 | 12 | module Bearing 13 | class IPC 14 | attr_reader :call_id 15 | 16 | def initialize(call_id: SecureRandom.uuid) 17 | @call_id = call_id 18 | end 19 | 20 | def wait_for_incoming_data 21 | res = nil 22 | 23 | Timeout.timeout(3) do 24 | while !res 25 | if File.exist?(unique_tmp_file) 26 | res = File.read(unique_tmp_file) 27 | else 28 | sleep 0.4 29 | end 30 | end 31 | end 32 | 33 | res 34 | rescue Timeout::Error 35 | ({ _success: false, _timeout: true }).to_json 36 | ensure 37 | # FileUtils.remove_dir(unique_tmp_folder) 38 | end 39 | 40 | def write_incoming_data_to_tmp_file(uri_string = '') 41 | uri = URI.parse(uri_string) 42 | is_success = (uri.path == '/success') 43 | output = query_to_json(uri.query, success: is_success) 44 | 45 | File.open(unique_tmp_file, 'w') { |f| f.puts output } 46 | end 47 | 48 | private 49 | 50 | def query_to_json(query_str = '', success: false) 51 | res = 52 | Hash[ 53 | CGI.parse(query_str).map do |k, v| 54 | new_v = 55 | begin 56 | JSON.parse(v[0]) 57 | rescue StandardError 58 | v[0] 59 | end 60 | [k, new_v] 61 | end 62 | ] 63 | res[:_success] = success 64 | res.to_json 65 | end 66 | 67 | def unique_tmp_file 68 | "#{unique_tmp_folder}/result.json" 69 | end 70 | 71 | def unique_tmp_folder 72 | tmp_path = "/tmp/#{::APP_BUNDLE_ID}/#{@call_id}" 73 | FileUtils.mkdir_p(tmp_path, mode: 0o700) unless Dir.exist?(tmp_path) 74 | tmp_path 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /src/bearing/cmd.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../config' 4 | 5 | module Bearing 6 | module Cmd 7 | class << self 8 | def print_usage 9 | puts < 43 | Home @ https://carlo.github.io/bearing 44 | EOTXT 45 | end 46 | 47 | def start_app 48 | # Start the app bundle so the first call to Bear won't time out 49 | system("open -b #{::APP_BUNDLE_ID}") 50 | end 51 | 52 | def validate_action_argument!(action = '') 53 | if !action || action == '--help' 54 | print_usage 55 | exit 0 56 | elsif ::AVAILABLE_BEAR_ACTIONS.include?(action) 57 | return 58 | end 59 | 60 | actions_list = 61 | ::AVAILABLE_BEAR_ACTIONS.sort.map { |a| "- #{a}" }.join("\n") 62 | 63 | puts <<~EOTXT 64 | ERROR: first argument (action) must be one of … 65 | 66 | #{ 67 | actions_list 68 | } 69 | 70 | Call '#{$0} --help' for usage information 71 | EOTXT 72 | exit 1 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bearing, a scripting helper for Bear 2 | 3 | ## What it does 4 | 5 | Bearing allows for scripting Bear. Out of the box, Bear can be automated using 6 | [its x-callback-url API](https://bear.app/faq/X-callback-url%20Scheme%20documentation/), 7 | meaning you call a URI 8 | (`open "bear://x-callback-url/[action]?[action parameters]&[x-callback parameters]"`) 9 | to do things. Bearing adds to that by slightly abstracting and enhancing the 10 | experience: 11 | 12 | 1. There's a dedicated `bearing` CLI tool that accepts plain arguments. You 13 | don't have to URL-encode anything. 14 | 2. The CLI tool returns callback responses as JSON. 15 | 16 | 17 | ## Usage 18 | 19 | ```bash 20 | /usr/local/bin/bearing ACTION [parameters] 21 | ``` 22 | 23 | `ACTION` is the Bear action to call (e.g., 'open-note', 'create', 'add-text', 24 | etc.) 25 | 26 | Parameters are passed as 'key=value' pairs, e.g. 'title="My new note"'. See 27 | [Bear's API documentation](https://bear.app/faq/X-callback-url%20Scheme%20documentation/) 28 | for available actions and parameters. 29 | 30 | Bearing consists of both an unobtrusive statusbar app and a CLI component. 31 | The former will automatically open when the latter is used; it is needed for 32 | getting return values from the Bear API. 33 | 34 | 35 | ## Examples 36 | 37 | Please note: the results below are formatted nicely for better readability, the 38 | actual JSON results are not pretty-printed. 39 | 40 | ### Opening a note 41 | 42 | ```bash 43 | /usr/local/bin/bearing \ 44 | open-note \ 45 | --id="123D41D6-E0F1-1234-1234-1B80D08074B7-12345-0000A0958DF5307C" 46 | ``` 47 | 48 | Returns: 49 | 50 | ```json 51 | { 52 | "creationDate": "2020-06-12T07:39:06Z", 53 | "title": "rtomayko/posix-spawn: Ruby process spawning library", 54 | "is_trashed": "no", 55 | "note": "# rtomayko/posix-spawn: Ruby process spawning library\nhttps://github.com/rtomayko/posix-spawn\n\n…", 56 | "identifier": "123D41D6-E0F1-1234-1234-1B80D08074B7-12345-0000A0958DF5307C", 57 | "modificationDate": "2020-06-12T07:39:06Z", 58 | "_success": true 59 | } 60 | ``` 61 | 62 | ### Creating a note 63 | 64 | ```bash 65 | /usr/local/bin/bearing \ 66 | create \ 67 | --title="Bearing test note" \ 68 | --text="Works for me!" 69 | ``` 70 | 71 | Returns: 72 | 73 | ```json 74 | { 75 | "title": "Bearing test note", 76 | "identifier": "4963F8B4-3FE0-4835-B96D-7DCCB6101A62-1917-00013DC4BC4EE819", 77 | "_success": true 78 | } 79 | ``` 80 | 81 | ### Searching 82 | 83 | ```bash 84 | /usr/local/bin/bearing 85 | search \ 86 | --term=macOS \ 87 | --token=ABCDEF-123456-A1B2C3 88 | ``` 89 | 90 | Returns: 91 | 92 | ```json 93 | { 94 | "notes": [ 95 | { 96 | "creationDate": "2020-06-14T10:09:00Z", 97 | "title": "Maccy - clipboard manager for macOS", 98 | "modificationDate": "2020-06-14T10:09:00Z", 99 | "identifier": "37FA3BEA-E670-1234-1234-9BA2EAB129FF-687-0000004F844AA84B", 100 | "pin": "no" 101 | }, 102 | { 103 | "creationDate": "2020-06-12T07:39:06Z", 104 | "title": "rtomayko/posix-spawn: Ruby process spawning library", 105 | "modificationDate": "2020-06-12T07:39:06Z", 106 | "identifier": "123D41D6-E0F1-1234-1234-1B80D08074B7-12345-0000A0958DF5307C", 107 | "pin": "no" 108 | }, 109 | { 110 | "creationDate": "2020-06-05T09:29:17Z", 111 | "title": "sbusso/Bear-Power-Pack: A collection of workflows enhancing Bear writer app on iOS and Mac.", 112 | "modificationDate": "2020-06-11T15:11:07Z", 113 | "identifier": "310C0689-50D2-1234-1234-F122397F8784-12345-00030C6A48F46C48", 114 | "pin": "no" 115 | }, 116 | … 117 | ] 118 | } 119 | ``` 120 | 121 | 122 | ## Installation 123 | 124 | Download [a release](https://github.com/carlo/bearing/releases), unpack it, move 125 | `Bearing.app` to your Applications folder, start it. 126 | 127 | Bearing is a menubar app, its icon is a "B". Click the icon, and select 128 | _"Install /usr/local/bin/bearing"_ from the menu. This will install a symlink 129 | in `/usr/local/bin/`. 130 | 131 | Now you can run `bearing` from your shell. 132 | 133 | 134 | ## Credits 135 | 136 | Bearing was made by Carlo Zottmann, , . 137 | 138 | 139 | ## License 140 | 141 | MIT. See [LICENSE.md](LICENSE.md). 142 | 143 | 144 | ## Acknowledgements 145 | 146 | [Bear](https://bear.app/) is a neat piece of software 147 | (c) [Shiny Frog, Inc.](http://www.shinyfrog.net/) Bearing and its 148 | author are neither affiliated nor endorsed by Shiny Frog. I'm just a fan of 149 | their stuff. 150 | 151 | The Bearing app was made using [Platypus](https://sveinbjorn.org/platypus), _"a 152 | developer tool that creates native Mac applications from command line scripts"_ 153 | by Sveinbjörn Þórðarson. Platypus is dope, trust me on that. 154 | 155 | --- 156 | 157 | ![app icon](assets/appicon.png) 158 | --------------------------------------------------------------------------------