;;; keyfreq.el --- track command frequencies ;; -*- coding: utf-8 -*- ;; ;; Copyright 2009-2010, 2015 by David Capello ;; Copyright 2008 by Xah Lee ;; Copyright 2006 by Michal Nazarewicz ;; Copyright 2006 by Ryan Yeske ;; ;; Author: Ryan Yeske, Michal Nazarewicz (mina86/AT/mina86.com) ;; Maintainer: David Capello, Xah lee ;; Created: 2006 ;; ;; Package-Requires: ((cl-lib "0.5")) ;; ;; ;; Keyfreq is free software; you can redistribute it and/or modify it ;; under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 3 of the License, or ;; (at your option) any later version. ;; ;; Version 1.7 - 2015-09 - David Capello ;; * Avoid accumulating key frequency if keyfreq-file-release-lock fails ;; ;; Version 1.6 - 2015-09 - David Capello ;; * Added keyfreq-reset thanks to @w-vi ;; * Fixed issue running multiple instances of Emacs 24.5 ;; ;; Version 1.5 - 2014-11 - David Capello ;; * Support cl-lib or cl ;; * Minor doc fixes ;; ;; Version 1.4 - 2010-09 - David Capello ;; * Renamed from command-frequency to keyfreq ;; * Now keyfreq-table holds "deltas" ;; * Removed a lot of "facility" functions to keep the code simple. ;; * Rename keyfreq-summarize-all-modes -> keyfreq-groups-major-modes. ;; * Added keyfreq-filter-major-mode ;; * Added lock file to avoid overwrite the file by two processes at ;; the same time. ;; ;; Version 1.3 - 2009-09 - David Capello ;; * Added keyfreq-summarize-all-modes ;; ;; Version 1.2 - 2009-09 - David Capello ;; * Now each hash hash-key is a (major-mode . command) cons. Now only ;; symbols are recorded. ;; ;; Version 1.1 - 2008-09 ;; - Replaced the use of this-command var by real-last-command, so ;; that the commands backward-kill-word, kill-word, kill-line, ;; kill-region, do not all get counted as kill-region. Changed ;; post-command-hook to pre-command-hook ;; ;; Version 1.0 - 2007 ;; - Made into a full featured minor mode. Added full doc ;; strings. Added feature to save and read to disk the frequency ;; hash table. Added ability to set user preference using emacs's ;; customization system. Code is ~400 lines. This version is made by ;; Michal Nazarewicz in 2007. ;; ;; Version 0.1 - 2006 ;; - First version by Ryan Yeske. A quick hack of about 40 lines. ;; ;;; Commentary: ;; ;; HOW TO USE IT? ;; ;; Include the following lines in your .emacs file: ;; ;; (require 'keyfreq) ;; (keyfreq-mode 1) ;; (keyfreq-autosave-mode 1) ;; ;; And use `keyfreq-show' to see how many times you used a command. ;; ;;; Code: (if (not (featurep 'cl-lib)) (progn (require 'cl) ;; fix conflict name (defalias 'cl-reduce 'reduce)) (require 'cl-lib)) ;; (require 'json)? (defgroup keyfreq nil "Customization group for Keyfreq mode. This mode stores number of times each command was called and provides it as a statistical data." :package-version '(keyfreq . "1.7") :group 'local :prefix "keyfreq") ;;;###autoload (define-minor-mode keyfreq-mode "Keyfreq mode records number of times each command was called making it possible to access usage statistics through various keyfreq-* functions." :global t :init-value nil :lighter nil :keymap nil :group 'keyfreq (if keyfreq-mode (add-hook 'pre-command-hook 'keyfreq-pre-command-hook) (remove-hook 'pre-command-hook 'keyfreq-pre-command-hook))) (defcustom keyfreq-buffer "*frequencies*" "Buffer where frequencies are displayed." :group 'keyfreq :type 'string) (defcustom keyfreq-file "~/.emacs.keyfreq" "File `keyfreq-table' is saved to/loaded from by `keyfreq-table-save' and `keyfreq-table-load' functions by default." :group 'keyfreq :type 'file) (defcustom keyfreq-file-lock "~/.emacs.keyfreq.lock" "Lock file to update the `keyfreq-file'." :group 'keyfreq :type 'file) (defcustom keyfreq-excluded-regexp '() "List of commands in REGEXP-FORM excluded by keyfreq." :group 'keyfreq :type 'file) (defvar keyfreq-table (make-hash-table :test 'equal :size 128) "Hash table storing number of times each command was called in each major mode since the last time the frequencies were saved in `keyfreq-file'.") (defvar keyfreq-excluded-commands '() "List of commands excluded by keyfreq.") (defun keyfreq-match-p (cmd) "Return t if CMD in `keyfreq-excluded-commands' or match `keyfreq-excluded-regexp'." ;; HACK Inspire by `consult--regexp-filter' (let ((filter (mapconcat (lambda (x) (concat "\\(?:" x "\\)")) keyfreq-excluded-regexp "\\|"))) (or (memq cmd keyfreq-excluded-commands) (when keyfreq-excluded-regexp (string-match-p filter (symbol-name cmd)))))) (defun keyfreq-pre-command-hook () "Record command execution in `keyfreq-table' hash." (let ((command real-last-command) count) (when (and command (symbolp command)) (setq count (gethash (cons major-mode command) keyfreq-table)) (unless (keyfreq-match-p command) (puthash (cons major-mode command) (if count (1+ count) 1) keyfreq-table) )))) (defun keyfreq-groups-major-modes (table) "Group major modes in TABLE by command. Return a hash table where each entry has COMMAND as key and COUNTER as value." (let ((new-table (make-hash-table :test 'equal :size 128))) (maphash (lambda (k v) (puthash (cdr k) (+ (gethash (cdr k) new-table 0) v) new-table)) table) new-table)) (defun keyfreq-filter-major-mode (table major-mode) "Leave the frequencies of the specified major mode. Return a hash table where each entry has COMMAND as key and COUNTER as value." (let ((new-table (make-hash-table :test 'equal :size 128))) (maphash (lambda (k v) (when (eq (car k) major-mode) (puthash (cdr k) (+ (gethash (cdr k) new-table 0) v) new-table))) table) new-table)) (defun keyfreq-used-major-modes (table) "Return a list with the used major modes (major modes contained in the TABLE)." (let ((list)) (maphash (lambda (k v) (add-to-list 'list (car k))) table) list)) (defun keyfreq-list (table &optional reverse limit) "Return a cons which car is sum of times any command was used and cdr is a list of (command . count) pairs. If REVERSE is nil, sort it starting from the most used command; if it is `no-sort' the list is not sorted; if it is non-nil and not `no-sort', sort it from the least used commands. If LIMIT is a positive number, only commands which were used more then LIMIT times will be added. If it is a negative number, only commands which were used less then -LIMIT times will be added." (let (l (sum 0)) (maphash (cond ((or (not (numberp limit)) (= limit 0)) (lambda (k v) (setq l (cons (cons k v) l) sum (+ sum v)))) ((= limit -1) (lambda (k v) (setq sum (+ sum v)))) ((< limit 0) (setq limit (- limit)) (lambda (k v) (setq sum (+ sum v)) (if (< v limit) (setq l (cons (cons k v) l))))) (t (lambda (k v) (setq sum (+ sum v)) (if (> v limit) (setq l (cons (cons k v) l)))))) table) (cons sum (cond ((equal reverse 'no-sort) l) (reverse (sort l (lambda (a b) (< (cdr a) (cdr b))))) (t (sort l (lambda (a b) (> (cdr a) (cdr b))))))))) (defun keyfreq-format-list (list &optional func) "Return formatted string with command usage statistics. The LIST is the `keyfreq-table' converted to a list using the `keyfreq-list'. If FUNC is nil each line contains number of times command was called and the command; if it is t percentage usage is added in the middle; if it is 'raw each line will contain number an command separated by single line (with no formatting) otherwise FUNC must be a function returning a string which will be called for each entry with three arguments: number of times command was called, percentage usage and the command." (let* ((sum (car list)) (max-len (cl-reduce (lambda (a b) (max a (length (symbol-name (car b))))) (cdr list) :initial-value 0))) (mapconcat (cond ((not func) (lambda (e) (format "%7d %s\n" (cdr e) (car e)))) ((equal func t) (lambda (e) (format (concat "%7d %6.2f%% %- " (format "%d" max-len) "s %s\n") (cdr e) (/ (* 1e2 (cdr e)) sum) (car e) (ignore-errors (keyfreq-where-is (car e)))))) ((equal func 'raw) (lambda (e) (format "%d %s\n" (cdr e) (car e)))) (t (lambda (e) (funcall func (cdr e) (/ (* 1e2 (cdr e)) sum) (car e))))) (cdr list) ""))) (defun keyfreq-where-is (command) (mapconcat 'key-description (where-is-internal command) ", ")) (defun keyfreq-show (&optional major-mode-symbol) "Show command usage statistics in `keyfreq-buffer'. If MAJOR-MODE-SYMBOL is given, the function shows the statistics for that particular major mode only. With a universal argument, the major-mode of the current buffer is used as MAJOR-MODE-SYMBOL argument." (interactive (list (cond (current-prefix-arg major-mode) (t nil)))) (let ((table (copy-hash-table keyfreq-table))) ;; Merge with the values in `keyfreq-file' (keyfreq-table-load table) (let* ((list (keyfreq-list (cond (major-mode-symbol (keyfreq-filter-major-mode table major-mode-symbol)) (t (keyfreq-groups-major-modes table))))) (formatted-list (keyfreq-format-list list t))) ;; Display the table (display-message-or-buffer (concat (if major-mode-symbol (concat "For " (symbol-name major-mode)) (concat "For all major modes")) ":\n\n" formatted-list) keyfreq-buffer) ))) (defun keyfreq-html (filename &optional confirm) "Save an HTML file as FILENAME with all the statistics of each mode." (interactive (list (if buffer-file-name (read-file-name "Write HTML file: " nil nil nil nil) (read-file-name "Write HTML file: " default-directory (expand-file-name (file-name-nondirectory (buffer-name)) default-directory) nil nil)) (not current-prefix-arg))) (and confirm (file-exists-p filename) (or (y-or-n-p (format "File `%s' exists; overwrite? " filename)) (error "Canceled"))) (let ((table (copy-hash-table keyfreq-table)) (htmltable (lambda (list) (insert "
| Times | Percetage | Command |
|---|---|---|
| %d | %.2f%% | %s |