Skip to content

noctuid/link-hint.el

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 

Repository files navigation

http:https://melpa.org/packages/link-hint-badge.svg

Demonstration

Using this package to install this package: http:https://noctuid.github.io/link-hint.el/assets/paradox_demonstration.gif

About

link-hint.el is inspired by the link hinting functionality in vim-like browsers and browser plugins such as pentadactyl. It provides commands for using avy to open, copy, or take a user-defined action on “links.”

Currently the following types of links are supported:

  • Plain text urls (e.g. http:https://github.com; includes irc urls and mailto)
  • File paths
  • Shr urls (e.g. elfeed links, links in html mu4e and gnus messages, eww urls, etc.)
  • Org mode urls (includes mailto: links)
  • Markdown links (including “wiki links”; see markdown-enable-wiki-links)
  • Mu4e links (urls and mailto addresses)
  • Mu4e attachments
  • Gnus html mail rendered with gnus-w3m or emacs-w3m (instead of shr)
  • Help mode links
  • Info mode links
  • Package menu links (describe package, install package, keyword buttons, etc.)
  • Compilation mode links
  • W3m links (urls, email addresses, etc.)
  • Customize links
  • Nov.el links
  • notmuch-hello links
  • Deadgrep matches
  • Other button links (e.g. WoMan/Man/Dictionary links, ag mode, epa-key-list-mode, etc.)
  • Completion List candidates (better put it before link-hint-file-link for project.el)
  • Dired filenames
  • Org Agenda items
  • Xref items
  • Bug reference mode links

Feel free to request support for any useful link type I may have missed. Also, if you think it would be beneficial to have a more specific link type split from a more generic link type, feel free to make an issue. For example, there may be some specific type of button you want to ignore or use in a custom command without affecting other buttons.

Similar

Ace-link

There is also ace-link which I didn’t know about when writing this package. The main functional differences at the time of writing are as follows:

  • link-hint supports more link types
  • link-hint supports operating on multiple different types of links with the same command in the same buffer; types are optionally tied to major modes, but generic link support is also provided (e.g. buttons and file paths)
  • link-hint supports easily adding more link types and actions
  • link-hint supports operating on multiple links at a time

Embark

There is some overlap with embark since both packages provide multiple actions for different types of things. Here is a comparison:

  • Link-hint is mainly focused on links/buttons rather than types of completing-reading candidates
  • Link-hint is more focused on selecting links anywhere visible using avy
  • Both embark and link-hint can take different actions on different types of things at the point in a buffer; link-hint can actually fall back to using embark-dwim (see At Point Fallback Commands)
  • If there is interest, link-hint can provide better integration with embark (provide feedback on issue #38 if interested)
    • Add completing-read support (with support for choosing an action with embark or running embark-collect)
    • Add a builtin link-hint action that just calls embark-act on the link position

Basic Setup

Basic usage of this package only requires making key bindings for link-hint-open-link or other commands. Here is an example configuration using use-package:

(use-package link-hint
  :ensure t
  :bind
  ("C-c l o" . link-hint-open-link)
  ("C-c l c" . link-hint-copy-link))

Here is an example configuration for evil:

(use-package link-hint
  :ensure t
  :defer t)

(define-key evil-normal-state-map (kbd "SPC f") 'link-hint-open-link)

Browser Choice

browse-url is used for opening urls, so in the case that the desired browser is not being used by default, the user can set browse-url-browser-function:

;; Use chromium to open urls
(setq browse-url-browser-function 'browse-url-chromium)

;; Use firefox to open urls
(setq browse-url-browser-function 'browse-url-firefox)

;; Use qutebrowser to open urls
(setq browse-url-browser-function 'browse-url-generic)
(setq browse-url-generic-program "qutebrowser")
;; Open urls in a new tab instead of window; can also be set in the config file
(setq browse-url-generic-args '("--target" "tab"))

Provided Commands

This package provides the following commands for operating on links:

  • link-hint-open-link-at-point - Open the link at point.
  • link-hint-copy-link-at-point - Copy the link at point to the kill ring (and optionally to the clipboard/primary).
  • link-hint-open-link - Use avy to select and open a single visible link. If only one link is currently visible, it will be automatically opened without the need for selection.
  • link-hint-open-multiple-links - Use avy to select multiple visible links and open them as soon as a key that does not correspond to a link (a key not in the avy overlay) is pressed (like pentadactyl’s g;).
  • link-hint-open-all-links - Opens all links visible in the buffer.
  • link-hint-copy-link - Use avy to select and copy a single visible link to the kill ring. select-enable-clipboard and select-enable-clipboard can each be set to a non-nil value to also use the clipboard and/or primary.

link-hint-copy-multiple-links and link-hint-copy-all-links also exist, but they may not be useful very often.

This package does not bind any commands by default.

At Point Fallback Commands

While the main purpose of link-hint is remote link selection with avy, it does provide commands to operate on a link at point (since it already has the necessary code to do so). link-hint-open-link-at-point, for example, can be used as a sort of global “act on the point” command. If there is not a link at the point, you can make it fall back to another command by setting link-hint-action-fallback-commands. Some potentially useful fallback commands would be embark-dwim and action-key (hyperbole).

To still get the “no links found” error message when nothing happens, fallback command should return nil if it also fails to do anything.

(setq link-hint-action-fallback-commands
      (list :open (lambda ()
                    (condition-case _
                        (progn
                          (embark-dwim)
                          t)
                      (error
                       nil)))))

Here is a more complex example that will try to jump to the definition in programming modes where possible before falling back to hyperbole. If you want something like this, you will probably need to tweak this rather than use it as-is.

(defun noct-open ()
  "Open the thing at point.
Try with lsp or smart jump (if in a prog-mode buffer) then with hyperbole."
  (interactive)
  (or (when (derived-mode-p 'prog-mode)
        (cond ((bound-and-true-p lsp-mode)
               (not (stringp (lsp-find-definition))))
              ((fboundp 'smart-jump-go)
               ;; return nil instead of prompting when there is no definition
               ;; at point
               (cl-letf (((symbol-function 'xref--prompt-p) #'ignore))
                 (smart-jump-go)))))
      (when (fboundp 'action-key)
        (action-key))))

(setq link-hint-action-fallback-commands (list :open #'noct-open))

Overriding Avy Settings

link-hint.el supports overriding avy’s settings. For example, if you want to use a different avy style just for link hinting, you can set link-hint-avy-style:

(setq link-hint-avy-style 'pre)

This will cause the overlays to be displayed before the links (and not cover them). Note that using the post style will not put the overlay at the end of links. I don’t think this style makes much sense for links, but feel free to open an issue if you would like this style to be supported.

Here is the full list of settings:

  • link-hint-avy-style
  • link-hint-avy-keys
  • link-hint-avy-all-windows
  • link-hint-avy-all-windows-alt
  • link-hint-avy-background
  • link-hint-avy-ignored-modes

By default, these variables are not bound, and avy’s corresponding settings are used. avy-styles-alist and avy-keys-alist are also supported for the provided commands (as well as avy-resume).

Messaging

By default, link-hint will print a message in the echo area when an action is performed. link-hint-message can be set to nil to disable this behavior. It can also be set to a custom message function such as lv-message.

link-hint-action-messages is a plist that is used for the default description of each action keyword (e.g. =:open “Opened”=).

Point/Window Restoration

Link hint will move the point (and sometimes the window; see avy-all-windows) when acting on a link. When link-hint-restore is a non-nil value, link-hint will automatically restore the point and window when the link action does not intentionally change the point/window. For example, if link-hint-avy-all-windows is a non-nil value, and the user copies a link in a different window, the point will stay the same in the buffer containing the link, and the selected window will stay the same. On the other hand, if the user opens a url in eww in a new window, the eww window will be selected, but the point in the link buffer will be restored. Similarly, if the user opens an org link to a local (same buffer) heading, the point and window will not be restored.

Defining New Link Types and Actions

link-hint-define-type is the helper function used to define new link types. link-hint-define-type is just simple helper to alter the symbol plist of link-hint-<type> (though it is recommended to use it directly in case the implementation changes). For example, here is how shr-url could be defined if it did not already exist:

(link-hint-define-type 'shr-url
  :next #'link-hint--next-shr-url
  :at-point-p #'link-hint--shr-url-at-point-p
  :open #'browse-url
  :copy #'kill-new)

(push 'link-hint-shr-url link-hint-types)

All link hint types are defined in this way, so see the source code for more examples.

Mandatory Keywords

:next

  • should be a function that returns the position of the next link after the point (i.e. if there is a link at the point, it should not return the point)
  • should not move the point (wrap your code in save-excursion if you move the point)
  • arglist: (bound) - one argument for the end bound for searching

:at-point-p

  • should be a function that returns a non-nil value if there is a link at the point
  • its return value can be used in the action functions
  • arglist: () - not passed any arguments

Predicate Keywords

These keywords are used to determine when a type is active. If these keywords are specified, link-hint will only check for the link type if the buffer meets the requirements. These are not strictly necessary but can be used, for example, to help performance (this is usually not an issue except for “overlay button” links currently - woman buttons, dictionary mode buttons, etc.).

:predicates should be a list of functions that should each return true if the link type passes/is valid in the current buffer.

:vars should be a list of variables and/or major modes. If at least one of them is bound and true or the current major mode, the link type passes.

:not-vars should be a list of variables and/or major modes. If any of them are bound and true or the current major mode, the link type does not pass.

All of these checks must pass for the link type to be considered active. It is also possible to create commands that only operate on specific link types by binding link-hint-types (e.g. (let ((link-hint-types ...)))).

Action Keywords

The main actions supported by default are :open and :copy. Custom action keywords can have any name not already used by link-hint, but you may want to give your keywords some unique prefix to ensure they do not clash in case link-hint adds new action types (e.g. :my-<action>).

:<action> (e.g. :open)

  • should be function that will perform an action on a link (e.g. open it in the case of :open)
  • arglist: (<at-point-p return list item 1> <at-point-p return list item 2> ...) or (<at-point-p return value as single argument>) or (); the function is not required to take a specific number of arguments
    • if you want to use information obtained in the :at-point-p call, you can give the action implementation function a non-empty arglist
      • if the :at-point-p function returns a list, you can use multiple arguments (one for each item in the list)
      • if your :at-point-p function returns a single value, you should use a single argument, e.g. the text-url link type’s :at-point-p function returns the url to open as a string, so the :open function can just be browse-url (which takes a url as an argument)
    • if you use an empty arglist, the function should operate at the link at point

Link types are not required to support all action keywords. If a link type does not support a particular action keyword, it will just be ignored for that action.

Action Modifier Keywords

:parse should be a function that takes two arguments: the return value of the link type’s :at-point-p function and the action keyword. It should return a valid input for the action function. This can be useful, for example, if the at-point-p function returns a plist, struct, etc. and each action function only needs part of it (see the definition of package-link for a concrete example).

:<action>-multiple should be a boolean value corresponding to whether it makes sense to perform the action on multiple links in a row.

:<action>-message should be a string that will be used instead of the normal message string. For example, :open-message "Installed"= is specified for the =package-install-link type.

:describe should be a function that returns a string representation of the link to be used when messaging. If not set, the return value of the :at-point-p function is used directly.

Creating New Commands

The user can create new commands to do something other than copy or open a link using the link-hint--one, link-hint--multiple, and link-hint--all helper functions. Each takes a single action keyword as an argument.

Here is an example of adding a command that downloads a url:

;; `link-hint-define-type' can be used to add new keywords
(link-hint-define-type 'text-url
  :download #'w3m-download)

(link-hint-define-type 'w3m-link
  :download #'w3m-download)

...

(defun link-hint-download-link ()
  "Use avy to select and download a text URL with download-region.el."
  (interactive)
  (avy-with link-hint-download-link
    (link-hint--one :download)))

Using for Links in Terminal with Tmux

This may seem like a strange use for this package, but I’ve been doing this due to lack of a better alternative. Unfortunately, most of the methods for generically opening urls in a terminal running tmux (e.g. urlscan, urlview, w3m, terminal keybindings, tmux-urlview, and tmux-open) aren’t very quick or customizable. tmux-fingers looks more promising but currently only supports copying, doesn’t allow for customizable hint keys, and is slow for me.

I’ve started using this keybinding on the rare occasion that I need to open a url from somewhere other than emacs:

bind-key f capture-pane \; save-buffer /tmp/tmux-buffer \; \
	new-window 'emacsclient -t -e "(find-file \"/tmp/tmux-buffer\")" -e "(goto-address-mode)" -e "(link-hint-open-link)" -e "(kill-this-buffer)" -e "(delete-frame)"'

I kill the buffer to ensure that emacs won’t prompt to revert the file on later invocations in the case that auto-revert-mode is off.

One downside (shared by most other methods) is that it may be a bit disorienting to have the positions of links moved when opening a new tmux window. In this regard, having link-opening functionality directly in a terminal is nice.