├── wiki_link_service.rb ├── README.md └── bookmarker /wiki_link_service.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Service script for opening wiki links in a text editor. 4 | # Works with bookmarker script and the bookmark CLI. Scans 5 | # for [[wiki links]]. Works with [[wiki links|display 6 | # text]]. If duti is installed, get app name for file 7 | # extension. 8 | 9 | BOOKMARKER = "~/scripts/bookmarker" 10 | DUTI = "/opt/homebrew/bin/duti" 11 | 12 | def parse_input(input) 13 | input.scan(/\[\[([a-z0-9 ]+)(?:\|.*?)?\]\]/i).flatten 14 | end 15 | 16 | def notify(message) 17 | `osascript -e 'display notification "#{message}" with title "Wiki Link Service"'` 18 | end 19 | 20 | def yn(message) 21 | `osascript -e 'button returned of (display dialog "#{message}" with title "Wiki Link Service" buttons {"Yes", "No"})'`.strip == "Yes" 22 | end 23 | 24 | ids = parse_input($stdin.read) 25 | 26 | if ids.empty? 27 | notify "No WikiLink found" 28 | exit 1 29 | end 30 | 31 | ids.each do |id| 32 | path = `#{BOOKMARKER} find "#{id}"`.strip 33 | 34 | if path.empty? 35 | # If bookmark isn't found, try to find it with Spotlight 36 | spotlight_path = `mdfind "description:#{id}"`.strip.split(/\n/).first 37 | path = spotlight_path if File.exist?(spotlight_path) 38 | 39 | if path.empty? 40 | notify "No bookmark found for #{first_id}" 41 | exit 42 | end 43 | end 44 | 45 | if File.directory?(path) 46 | `open -R "#{path}"` if yn("Open #{File.basename(path)} in Finder?") 47 | exit 0 48 | end 49 | 50 | app = "default application" 51 | 52 | if File.executable?(DUTI) 53 | app = `#{DUTI} -x #{File.extname(path).delete(".")}`.split(/\n/).first.sub(/\.app$/, "") 54 | end 55 | 56 | `open "#{path}"` if yn("Open #{File.basename(path)} in #{app}?") 57 | end 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bookmarker 2 | 3 | [bookmark-cli]: https://github.com/ttscoff/bookmark-cli 4 | 5 | This script is designed to work with [bookmark-cli]. It creates a JSON reference file allowing for short bookmark names to reference files. The links are "sturdy" and will survive the target file being moved or renamed. 6 | 7 | The original version of this script was created by Ralf Hülsmann. 8 | 9 | ## Installation 10 | 11 | Copy the `bookmarker` file to your $PATH and make sure it's executable with `chmod a+x /path/to/bookmarker`. 12 | 13 | I prefer to make symlinks to scripts like this so that I can pull changes in the original repo directory and my script updates automatically: 14 | 15 | ln -s /path/to/bookmarker/bookmarker /opt/homebrew/bin/ 16 | 17 | 18 | ## Usage 19 | 20 | ```console 21 | Usage: 22 | bookmarker add|save /path/to/file [alias] → Save bookmark 23 | bookmarker get|find 123456789 → Retrieve bookmark 24 | bookmarker delete|x 123456789 → Delete bookmark 25 | bookmarker list|ls → Show all bookmarks 26 | ``` 27 | 28 | If an alias is not passed to the add subcommand, a numeric id will be generated automatically. 29 | 30 | When running the add command, the resulting bookmark is output to STDOUT. The key will be downcased and spaces will be removed, so the actual key may be different than the input. Passing the command to `pbcopy` will therefore end with the new key in the clipboard. 31 | 32 | Passing an alias or numeric id to the `get` subcommand will output the resulting path to STDOUT, so you can pass the result to another command (e.g. `pbcopy` or an `open` command). 33 | 34 | If a bookmarked file exists in a synced directory that matches the filesystem on another machine, the bookmark will work on the other machine as well. 35 | 36 | ## System Service 37 | 38 | The included wiki_link_service.rb file can be used in a macOS System Service (Quick Action) to allow wiki linking from any application, e.g. `[[my bookmark]]`, where "my bookmark" is an alias created by the `bookmarker` script. 39 | 40 | To create a System Service: 41 | 42 | 1. Open Automator and create a new Quick Action. You can leave all the settings at their default. 43 | 2. Add a `Run shell script` action to the service 44 | 3. Set the interpreter to `/usr/bin/ruby` 45 | 4. Paste the contents of `wiki_link_service` to the Run Shell Script action 46 | 5. Save the Service as "Open Wiki Link" 47 | 48 | Now you can select text containing a `[[wiki link]]` that references a bookmarked file, right click, and select Services->Open Wiki Link. The bookmarked file will be opened in its default application. -------------------------------------------------------------------------------- /bookmarker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "json" 3 | require "securerandom" 4 | require "optparse" 5 | 6 | # Created by Ralf Hulsmann 7 | # 8 | # A wrapper around 9 | # [bookmark-cli](https://github.com/ttscoff/bookmark-cli) to 10 | # save and retrieve bookmarks with a short ID. 11 | # 12 | # Modifications by Brett Terpstra 13 | # 14 | # - Translated to English 15 | # - Added aliases for commands, simple one-letter 16 | # subcommands will work, e.g. `bookmarker l` to list 17 | # - When outputting a list, show resolved path instead of 18 | # bookmark blob. Since this wrapper is meant to abstract 19 | # bookmark-cli, there"s probably no point in ever 20 | # returning the blob. 21 | # - When listing, separate ids and paths with a tab to make 22 | # it easier to parse 23 | # - Made output commands use warn for messages and print to 24 | # STDOUT for just the id or path to make it easier to use 25 | # in pipelines 26 | # - Save bookmarks JSON to ~/.local/share/bookmarks.json 27 | # - Hardcode path to bookmark binary, change as needed 28 | # - Allow user to manually specify a key by passing a string 29 | # after the path in the `add` command 30 | # - Add --quiet/-q option to silence verbose output messages 31 | # - Strip spaces and downcase ID arguments, so "Boom Chicka" 32 | # becomes boomchicka for both adding and searching. This 33 | # allows, e.g., for a [[Boom Chicka]] wiki link 34 | 35 | ## Configuration 36 | # Path to `bookmark` binary (result of `which bookmark`) 37 | BOOKMARK = "/opt/homebrew/bin/bookmark" 38 | # Path to JSON file, don't change unless you know what you're doing 39 | BOOKMARK_FILE = File.expand_path("~/.local/share/bookmarks.json") 40 | 41 | # Load JSON file or create new structure 42 | def load_bookmarks 43 | File.exist?(BOOKMARK_FILE) ? JSON.parse(File.read(BOOKMARK_FILE)) : {} 44 | rescue JSON::ParserError 45 | {} 46 | end 47 | 48 | # Save JSON file 49 | def save_bookmarks(bookmarks) 50 | File.directory?(File.dirname(BOOKMARK_FILE)) || FileUtils.mkdir_p(File.dirname(BOOKMARK_FILE)) 51 | File.write(BOOKMARK_FILE, JSON.pretty_generate(bookmarks)) 52 | end 53 | 54 | # Generates a 9-digit random ID 55 | def generate_id 56 | rand(100_000_000..999_999_999).to_s 57 | end 58 | 59 | # Set Finder metadata (for Spotlight search) 60 | def set_spotlight_metadata(path, id) 61 | existing = `mdls --name kMDItemDescription #{path}`.strip 62 | id = "#{existing} #{id}" if existing !~ /\(null\)/ 63 | system("xattr -w com.apple.metadata:kMDItemDescription \"#{id}\" \"#{path}\"") 64 | end 65 | 66 | # Save bookmark with `bookmark save` 67 | def add_bookmark(path, id = nil) 68 | bookmarks = load_bookmarks 69 | id ||= generate_id 70 | 71 | while bookmarks.key?(id) # Ensure ID is unique 72 | if id =~ /^\d+$/ 73 | id = generate_id 74 | else 75 | id = id =~ /-\d+$/ ? id.next : "#{id}-2" 76 | end 77 | end 78 | 79 | # Call `bookmark save` 80 | bookmark_id = `#{BOOKMARK} save "#{path}"`.strip 81 | if bookmark_id.empty? 82 | puts "Error saving the bookmark." 83 | exit(1) 84 | end 85 | 86 | # Set Finder metadata 87 | set_spotlight_metadata(path, id) 88 | 89 | bookmarks[id] = bookmark_id 90 | save_bookmarks(bookmarks) 91 | 92 | warn "Bookmark saved with ID: #{id}" 93 | print id 94 | end 95 | 96 | # Retrieve bookmark with `bookmark find` 97 | def get_bookmark(id) 98 | bookmarks = load_bookmarks 99 | if bookmarks.key?(id) 100 | bookmark_id = bookmarks[id] 101 | path = `#{BOOKMARK} find #{bookmark_id}`.strip 102 | if path.empty? 103 | warn "No valid path found." 104 | else 105 | warn "#{id}: #{path}" 106 | print path 107 | end 108 | else 109 | warn "No bookmark found with this ID." 110 | end 111 | end 112 | 113 | # Delete bookmark 114 | def delete_bookmark(id) 115 | bookmarks = load_bookmarks 116 | if bookmarks.key?(id) 117 | path = `#{BOOKMARK} find #{bookmarks[id]}`.strip 118 | if !path.empty? 119 | system("xattr -d com.apple.metadata:kMDItemDescription \"#{path}\"") # Delete metadata 120 | end 121 | bookmarks.delete(id) 122 | save_bookmarks(bookmarks) 123 | warn "Bookmark with ID #{id} deleted." 124 | else 125 | warn "No bookmark found with this ID." 126 | end 127 | end 128 | 129 | # Display all bookmarks 130 | def list_bookmarks 131 | bookmarks = load_bookmarks 132 | if bookmarks.empty? 133 | warn "No saved bookmarks." 134 | else 135 | puts "Saved bookmarks:" 136 | bookmarks.each do |id, bookmark_id| 137 | path = `#{BOOKMARK} find #{bookmark_id}`.strip 138 | puts "#{id}\t#{path.empty? ? "Invalid bookmark" : path}" 139 | end 140 | end 141 | end 142 | 143 | # CLI control 144 | command = ARGV[0] 145 | ARGV.shift 146 | argument = ARGV[0]&.downcase&.gsub(/ +/, "") if ARGV[0] 147 | ARGV.shift 148 | 149 | id = ARGV.length.positive? ? ARGV[0].downcase.gsub(/ +/, "") : generate_id 150 | 151 | $options = { quiet: false } 152 | parser = OptionParser.new do |opts| 153 | opts.on("-q", "--quiet", "Suppress output messages") { $options[:quiet] = true } 154 | end 155 | 156 | parser.parse! 157 | 158 | def warn(msg) 159 | $stderr.puts msg unless $options[:quiet] 160 | end 161 | 162 | case command 163 | when /^[as]/i # Add or Save 164 | if argument 165 | add_bookmark(argument, id) 166 | else 167 | puts "Specify path: `#{File.basename(__FILE__)} add /path/to/file [alias]`" 168 | end 169 | when /^[gf]/i # Get or Find 170 | if argument 171 | get_bookmark(argument) 172 | else 173 | puts "Specify ID: `#{File.basename(__FILE__)} get 123456789`" 174 | end 175 | when /^[dx]/i # Delete or X 176 | if argument 177 | delete_bookmark(argument) 178 | else 179 | puts "Specify ID: `#{File.basename(__FILE__)} delete 123456789`" 180 | end 181 | when /^l/ 182 | list_bookmarks 183 | else 184 | puts "Usage:" 185 | puts " #{File.basename(__FILE__)} add|save /path/to/file [alias] → Save bookmark" 186 | puts " #{File.basename(__FILE__)} get|find 123456789 → Retrieve bookmark" 187 | puts " #{File.basename(__FILE__)} delete|x 123456789 → Delete bookmark" 188 | puts " #{File.basename(__FILE__)} list|ls → Show all bookmarks" 189 | end 190 | --------------------------------------------------------------------------------