#!/usr/bin/env ruby # frozen_string_literal: true require 'gli' require 'hook' # Main class for GLI app class App extend GLI::App program_desc 'CLI interface for Hook.app (macOS)' program_long_desc 'Hook.app is a productivity tool for macOS . This gem includes a `hook` binary that allows interaction with the features of Hook.app.' default_command 'help' autocomplete_commands = true synopsis_format(:terminal) version Hook::VERSION subcommand_option_handling :normal arguments :strict hooker = nil desc 'List hooks on a file or url' long_desc %{ Output a list of all hooks attached to given url(s) or file(s) in the specified format (default "paths"). Run `hook list` with no file/url argument to list all bookmarks.} arg_name 'FILE_OR_URL [FILE_OR_URL...]' command %i[list ls] do |c| c.desc 'Generate a menu to select hook(s) for opening' c.long_desc 'This option is a shortcut to `hook select` and overrides any other arguments.' c.switch %i[s select] c.desc 'Output only bookmarks with file paths (exclude e.g. emails)' c.switch %i[f files_only], { negatable: false, default_value: false } c.desc 'Separate results with NULL separator, only applies with "paths" output for single file argument' c.switch %i[null], { negatable: false, default_value: false } valid_formats = %w[hooks paths markdown verbose] fmt_list = valid_formats.map { |fmt| fmt.sub(/^(.)(.*?)$/, '(\1)\2') }.join(', ') c.desc "Output format [#{fmt_list}]" c.flag %i[o output_format], { arg_name: 'format', default_value: 'paths' } c.action do |_global_options, options, args| if options[:s] return hooker.open_linked(args[0]) end valid_format = hooker.validate_format(options[:o], valid_formats) exit_now!("Invalid output format: \"#{options[:o]}\"", 6) unless valid_format result = hooker.linked_bookmarks(args, { files_only: options[:f], format: valid_format, null_separator: options[:null] }) puts result end end # Shell completion scripts are located in lib/completion/ and named "hook_completion" with # the full shell name as the extension, e.g. "hook_completion.bash". desc 'Shell completion examples' valid_shells = %w[bash zsh fish] long_desc %{ Output completion script example for the specified shell (#{valid_shells.join(', ')}) } arg_name 'SHELL' command %i[scripts] do |c| c.action do |_global_options, _options, args| if args.length > 1 exit_now!("Invalid number of arguments, (expected 1)", 5) elsif args.nil? || args.empty? exit_now!("Specify a shell (#{valid_shells.join(', ')})", 0) else if valid_shells.include?(args[0]) base_dir = File.expand_path(File.join(File.dirname(__FILE__), '../lib/completion')) completion = File.join(base_dir, "hook_completion.#{args[0]}") script = IO.read(completion) $stdout.puts script else exit_now!("Invalid shell name, must be one of #{valid_shells.join(', ')}", 1) end end end end desc 'Search bookmarks' long_desc %{ Search bookmark urls and names for a string and output in specified format (default "paths"). Run `hook find` with no search argument to list all bookmarks.} arg_name 'SEARCH_STRING' command %i[find search] do |c| c.desc 'Output only bookmarks with file paths (exclude e.g. emails)' c.switch %i[f files_only], { negatable: false, default_value: false } c.desc 'Separate results with NULL separator, only applies with "paths" output for single file argument' c.switch %i[null], { negatable: false, default_value: false } valid_formats = %w[hooks paths markdown verbose] fmt_list = valid_formats.map { |fmt| fmt.sub(/^(.)(.*?)$/, '(\1)\2') }.join(', ') c.desc "Output format [#{fmt_list}]" c.flag %i[o output_format], { arg_name: 'format', default_value: 'paths' } c.desc "Search only bookmark names" c.switch %i[n names_only], { negatable: false, default_value: false } c.action do |_global_options, options, args| valid_format = hooker.validate_format(options[:o], valid_formats) exit_now!("Invalid output format: \"#{options[:o]}\"", 6) unless valid_format result = hooker.search_bookmarks(args.join(" "), { files_only: options[:f], format: valid_format, null_separator: options[:null], names_only: options[:n] }) puts result end end desc 'Create bidirectional hooks between two or more files/urls' long_desc %{ If two files/urls are provided, links will be bi-directional. If three or more are provided, `link` defaults to creating bi-directional links between each file and the last file in the list. Use `-a` to create bi-directional links between every file in the list. If using `--paste`, the URL/hook link in the clipboard will be used as one argument, to be combined with one or more file/url arguments. } arg_name 'SOURCE [SOURCE...] TARGET' command %i[link ln] do |c| c.desc 'Link every listed file or url to every other' c.switch %i[a all], { negatable: false, default_value: false } c.desc 'Paste URL from clipboard' c.switch %i[p paste], { negatable: false, default_value: false } c.action do |_global_options, options, args| if options[:p] clipboard = `pbpaste`.strip clipboard.valid_hook! args.push(clipboard) if clipboard end if args.length < 2 exit_now!('Wrong number of arguments. At least 2 files must be specified, or one file with --paste', 5) end if options[:a] puts hooker.link_all(args) else puts hooker.link_files(args) end end end desc 'Copy Hook URL for file/url to clipboard' long_desc %{ Creates a bookmark for the specified file or URL and copies its Hook URL to the clipboard. The copied Hook URL can be used to link to other files (use `hook link --paste FILE/URL), or to paste into another app as a link. Use the -m flag to copy a full Markdown link. } arg_name 'FILE_OR_URL' command %i[clip cp] do |c| c.desc 'Copy as Markdown' c.switch %i[m markdown], { negatable: false, default_value: false } c.desc 'Copy from application' c.flag %i[a app], { arg_name: 'APP_NAME' } c.action do |_global_options, options, args| exit_now!('Wrong number of arguments. Requires a path/url or -a APP_NAME', 5) if args.length != 1 && !options[:a] if options[:a] puts hooker.bookmark_from_app(options[:a], { copy: true, markdown: options[:m] }) else puts hooker.clip_bookmark(args[0], { markdown: options[:m] }) end end end desc 'Get a Hook URL for the frontmost window of an app' long_desc %{ Specify an application by name (without '.app') to bring that app to the foreground and create a bookmark for the active document, note, task, etc., returning a Hook URL. Use -m to get the response as Markdown, and/or -c to copy the result directly to the clipboard. } arg_name 'APPLICATION_NAME' command %i[from] do |c| c.desc 'Output as Markdown' c.switch %i[m markdown], { negatable: false, default_value: false } c.desc 'Copy to clipboard' c.switch %i[c copy], { negatable: false, default_value: false } c.action do |_global_options, options, args| exit_now!("Wrong number of arguments (1 expected, #{args.length} given)", 5) if args.length != 1 puts hooker.bookmark_from_app(args[0], { copy: options[:c], markdown: options[:m] }) end end desc 'Remove a hook between two files/urls' long_desc %{ Remove a hook between two files or URLs. If you use --all, all hooks on a given file will be removed. If --all isn't specified, exactly two arguments (Files/URLs) are required. } arg_name 'ITEM_1 ITEM_2' command %i[remove rm] do |c| c.desc 'Remove ALL links on files, requires confirmation' c.switch %i[a all], { negatable: false, default_value: false } c.action do |_global_options, options, args| result = hooker.delete_hooks(args, { all: options[:a] }) puts result end end desc 'Clone all hooks from one file or url onto another' long_desc %{ Copy all the files and urls that the first file is hooked to onto another file. Exactly two arguments (SOURCE, TARGET) required. } arg_name 'SOURCE TARGET' command %i[clone] do |c| c.action do |_global_options, _options, args| exit_now!("Wrong number of arguments. Two file paths or urls required (#{args.length} given)", 5) if args.length != 2 result = hooker.clone_hooks(args) puts result end end desc 'Select from hooks on a file/url and open in default application' long_desc %{ If the target file/URL has hooked items, a menu will be provided. Selecting one or more files from this menu will open the item(s) using the default application assigned to the filetype by macOS. Allows multiple selections with tab key, and type-ahead fuzzy filtering of results. } arg_name 'FILE_OR_URL' command %i[select] do |c| c.action do |_global_options, _options, args| exit_now!("Wrong number of arguments. One file path or url required (#{args.length} given)", 5) if args.length != 1 hooker.open_linked(args[0]) end end desc 'Open the specified file or url in Hook GUI' long_desc %{ Opens Hook.app on the specified file/URL for browsing and performing actions. Exactly one argument (File/URL) required. } arg_name 'FILE_OR_URL' command %i[open gui] do |c| c.action do |_global_options, _options, args| exit_now!("Wrong number of arguments. One file path or url required (#{args.length} given)", 5) if args.length != 1 hooker.open_gui(args[0]) end end pre do |global, _command, _options, _args| # Pre logic here # Return true to proceed; false to abort and not call the # chosen command # Use skips_pre before a command to skip this block # on that command only hooker = Hooker.new true end post do |_global, _command, _options, _args| # Post logic here # Use skips_post before a command to skip this # block on that command only end on_error do |exception| # Error logic here # return false to skip default error handling true end end exit App.run(ARGV)