├── .github ├── dependabot.yml └── workflows │ └── ci.yaml ├── .gitignore ├── README.md ├── avy-menu.el ├── flake.lock └── flake.nix /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | types: 8 | - opened 9 | - synchronize 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: cachix/install-nix-action@v31 16 | - run: nix build 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-autoloads.el 2 | *.elc 3 | *~ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Avy Menu 2 | 3 | [![License GPL 3](https://img.shields.io/badge/license-GPL_3-green.svg)](http://www.gnu.org/licenses/gpl-3.0.txt) 4 | [![MELPA](https://melpa.org/packages/avy-menu-badge.svg)](https://melpa.org/#/avy-menu) 5 | [![CI](https://github.com/mrkkrp/avy-menu/actions/workflows/ci.yaml/badge.svg)](https://github.com/mrkkrp/avy-menu/actions/workflows/ci.yaml) 6 | 7 | The library provides an [`avy`](https://github.com/abo-abo/avy)-powered 8 | popup menu. It is used in (at least) the following packages: 9 | 10 | * [`ace-popup-menu`](https://github.com/mrkkrp/ace-popup-menu) 11 | * [`char-menu`](https://github.com/mrkkrp/char-menu) 12 | * [`hasky-extensions`](https://github.com/hasky-mode/hasky-extensions) 13 | 14 | It can also be used directly. 15 | 16 | ## Installation 17 | 18 | The package is available via MELPA, so you can just type `M-x 19 | package-install RET avy-menu RET`. 20 | 21 | If you would like to install the package manually, download or clone it and 22 | put on Emacs' `load-path`. Then you can require it in your init file like 23 | this: 24 | 25 | ```emacs-lisp 26 | (require 'avy-menu) 27 | ``` 28 | 29 | Don't forget to include it in your list of dependencies if you are writing 30 | an Emacs Lisp package: 31 | 32 | ```emacs-lisp 33 | ;; Package-Requires: ((emacs "24.5") (avy-menu "0.1")) 34 | ``` 35 | 36 | ## API 37 | 38 | See the description of `avy-menu` in the source code or by typing `C-h f 39 | avy-menu RET`. 40 | 41 | ## Customization 42 | 43 | Use `M-x customize-group avy-menu RET` to change appearance of the menu. 44 | 45 | ## License 46 | 47 | Copyright © 2016–present Mark Karpov 48 | 49 | Distributed under GNU GPL, version 3. 50 | -------------------------------------------------------------------------------- /avy-menu.el: -------------------------------------------------------------------------------- 1 | ;;; avy-menu.el --- Library providing avy-powered popup menu -*- lexical-binding: t; -*- 2 | ;; 3 | ;; Copyright © 2016–present Mark Karpov 4 | ;; 5 | ;; Author: Mark Karpov 6 | ;; URL: https://github.com/mrkkrp/avy-menu 7 | ;; Version: 0.1.1 8 | ;; Package-Requires: ((emacs "24.4") (avy "0.4.0")) 9 | ;; Keywords: convenience 10 | ;; 11 | ;; This file is not part of GNU Emacs. 12 | ;; 13 | ;; This program is free software: you can redistribute it and/or modify it 14 | ;; under the terms of the GNU General Public License as published by the 15 | ;; Free Software Foundation, either version 3 of the License, or (at your 16 | ;; option) any later version. 17 | ;; 18 | ;; This program is distributed in the hope that it will be useful, but 19 | ;; WITHOUT ANY WARRANTY; without even the implied warranty of 20 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 21 | ;; Public License for more details. 22 | ;; 23 | ;; You should have received a copy of the GNU General Public License along 24 | ;; with this program. If not, see . 25 | 26 | ;;; Commentary: 27 | 28 | ;; The library provides an Avy-powered popup menu. It is used in (at least) 29 | ;; the following packages: 30 | ;; 31 | ;; * `ace-popup-menu' 32 | ;; * `char-menu' 33 | ;; * `hasky-extensions' 34 | ;; 35 | ;; It can also be used directly. 36 | 37 | ;;; Code: 38 | 39 | (require 'avy) 40 | (require 'cl-lib) 41 | 42 | (defgroup avy-menu nil 43 | "Avy-powered popup menu." 44 | :group 'convenience 45 | :tag "Avy Menu" 46 | :prefix "avy-menu-" 47 | :link '(url-link :tag "GitHub" "https://github.com/mrkkrp/avy-menu")) 48 | 49 | (defface avy-menu-title 50 | '((t (:inherit font-lock-function-name-face))) 51 | "Face used to print title of entire menu.") 52 | 53 | (defface avy-menu-pane-header 54 | '((t (:inherit underline))) 55 | "Face used to print pane headers.") 56 | 57 | (defface avy-menu-inactive 58 | '((t (:inherit shadow))) 59 | "Face used to print inactive menu items.") 60 | 61 | ;;;###autoload 62 | (defun avy-menu (buffer-or-name menu &optional show-pane-header) 63 | "Show a popup menu in a temporary window and return user's selection. 64 | 65 | BUFFER-OR-NAME specifies the name of the buffer (or the buffer 66 | itself) that hosts the menu options. MENU should be a list of 67 | the form (TITLE PANE1 PANE2 …), where each pane is a list of the 68 | form (TITLE ITEM1 ITEM2 …). Each item is normally a cons 69 | cell (STRING . VALUE), but a string can appear as an item—that 70 | adds a non-selectable item in the menu. Also, empty strings 71 | start new sub-sections. 72 | 73 | If SHOW-PANE-HEADER is not NIL, show pane headers (titles), 74 | otherwise hide them. 75 | 76 | The returned value is VALUE if user has selected something and 77 | NIL if they have cancelled the menu or pressed a key that does 78 | not correspond to an option in the menu." 79 | (let ((buffer (get-buffer-create buffer-or-name)) 80 | menu-item-alist 81 | (first-pane t)) 82 | (with-current-buffer buffer 83 | (with-current-buffer-window 84 | ;; buffer or name 85 | buffer 86 | ;; action (for `display-buffer') 87 | (cons 'display-buffer-below-selected 88 | '((window-height . fit-window-to-buffer) 89 | (preserve-size . (nil . t)))) 90 | ;; quit-function 91 | (lambda (window _value) 92 | (with-selected-window window 93 | (unwind-protect 94 | (cdr 95 | (assq 96 | (avy-with avy-menu 97 | (avy-process (mapcar #'car menu-item-alist) 98 | #'avy--overlay-pre)) 99 | menu-item-alist)) 100 | (when (window-live-p window) 101 | (quit-restore-window window 'kill))))) 102 | ;; menu generation 103 | (setq cursor-type nil) 104 | (cl-destructuring-bind (title . panes) menu 105 | (insert (propertize title 'face 'avy-menu-title) 106 | "\n\n") 107 | (dolist (pane panes) 108 | (cl-destructuring-bind (title . items) pane 109 | (if first-pane 110 | (setq first-pane nil) 111 | (insert "\n\n")) 112 | (when show-pane-header 113 | (insert (propertize title 'face 'avy-menu-pane-header) 114 | "\n\n")) 115 | (let ((pane-alist (avy-menu--insert-strings items))) 116 | (if menu-item-alist 117 | (nconc menu-item-alist pane-alist) 118 | (setq menu-item-alist pane-alist)))))))))) 119 | 120 | (defun avy-menu--insert-strings (items) 121 | "Insert ITEMS much like `completion--insert-strings' in the current buffer. 122 | 123 | ITEMS should be a list, where every element is a cons of the 124 | form (STRING . VALUE), where STRING is the string to be printed 125 | in the current buffer and VALUE is used to construct the result 126 | value of this function. ITEMS can contain plain strings, in this 127 | case they are printed with inactive face. Empty strings are not 128 | printed, instead they begin new sub-sections. 129 | 130 | Return an alist of values (POS . VALUE), where POS indicates the 131 | position of STRING in the buffer and VALUE is its associated 132 | value according to ITEMS." 133 | (when (consp items) 134 | (let* ((strings (mapcar (lambda (x) (if (consp x) (car x) x)) 135 | items)) 136 | (length (apply 'max 137 | (mapcar #'string-width strings))) 138 | (window (get-buffer-window (current-buffer) 0)) 139 | (wwidth (if window (1- (window-width window)) 79)) 140 | (columns (min (max 2 (/ wwidth (+ 2 length))) 141 | (max 1 (/ (length strings) 2)))) 142 | (colwidth (/ wwidth columns)) 143 | (column 0) 144 | (first t) 145 | laststring 146 | result) 147 | (dolist (str strings) 148 | (unless (equal laststring str) 149 | (setq laststring str) 150 | (let ((length (string-width str)) 151 | (value (cdr (assq str items)))) 152 | (unless first 153 | (if (or (< wwidth (+ (max colwidth length) column)) 154 | (zerop length)) 155 | (progn 156 | (insert "\n" (if (zerop length) "\n" "")) 157 | (setq column 0)) 158 | (insert " \t") 159 | (set-text-properties (1- (point)) (point) 160 | `(display (space :align-to ,column))))) 161 | (setq first (zerop length)) 162 | (when value 163 | (push (cons (point) value) result)) 164 | (insert (if value 165 | str 166 | (propertize str 'face 'avy-menu-inactive))) 167 | (setq column (+ column 168 | (* colwidth (ceiling length colwidth))))))) 169 | (reverse result)))) 170 | 171 | (provide 'avy-menu) 172 | 173 | ;;; avy-menu.el ends here 174 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "emacs-package-flake": { 4 | "inputs": { 5 | "flake-utils": "flake-utils", 6 | "nixpkgs": "nixpkgs" 7 | }, 8 | "locked": { 9 | "lastModified": 1686064298, 10 | "narHash": "sha256-AmhZ5UPCdEyUuYCKuNuga59YDNJmyyjImYc/J5wo4vE=", 11 | "owner": "mrkkrp", 12 | "repo": "emacs-package-flake", 13 | "rev": "ffeea4f1aa7d32eb09e53772e183f73ebda72cfd", 14 | "type": "github" 15 | }, 16 | "original": { 17 | "owner": "mrkkrp", 18 | "repo": "emacs-package-flake", 19 | "type": "github" 20 | } 21 | }, 22 | "flake-utils": { 23 | "inputs": { 24 | "systems": "systems" 25 | }, 26 | "locked": { 27 | "lastModified": 1685518550, 28 | "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", 29 | "owner": "numtide", 30 | "repo": "flake-utils", 31 | "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", 32 | "type": "github" 33 | }, 34 | "original": { 35 | "owner": "numtide", 36 | "repo": "flake-utils", 37 | "type": "github" 38 | } 39 | }, 40 | "nixpkgs": { 41 | "locked": { 42 | "lastModified": 1685931219, 43 | "narHash": "sha256-8EWeOZ6LKQfgAjB/USffUSELPRjw88A+xTcXnOUvO5M=", 44 | "owner": "NixOS", 45 | "repo": "nixpkgs", 46 | "rev": "7409480d5c8584a1a83c422530419efe4afb0d19", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "id": "nixpkgs", 51 | "ref": "nixos-unstable", 52 | "type": "indirect" 53 | } 54 | }, 55 | "root": { 56 | "inputs": { 57 | "emacs-package-flake": "emacs-package-flake" 58 | } 59 | }, 60 | "systems": { 61 | "locked": { 62 | "lastModified": 1681028828, 63 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 64 | "owner": "nix-systems", 65 | "repo": "default", 66 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "nix-systems", 71 | "repo": "default", 72 | "type": "github" 73 | } 74 | } 75 | }, 76 | "root": "root", 77 | "version": 7 78 | } 79 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | emacs-package-flake.url = "github:mrkkrp/emacs-package-flake"; 4 | }; 5 | outputs = { self, emacs-package-flake }: 6 | emacs-package-flake.lib.mkOutputs { 7 | name = "avy-menu"; 8 | srcDir = ./.; 9 | deps = ["avy"]; 10 | }; 11 | } 12 | --------------------------------------------------------------------------------