Files
eyebrowse/eyebrowse.el
Vasilij Schneidermann 0b02e02e8e Turn window configuration switcher into command
While it is possible to use `eyebrowse-next-window-config` with a
numerical prefix to switch to an arbitrary window configuration, it's
better to offer a completing interface that can be extended to display
tags as well.  I intend to offer `completing-read`,
`ido-completing-read` and helm soon to have a basic, a less basic, but
built-in with flexible matching and a fully-featured selection
interface that is able to display meta data, too.  At the moment only
`completing-read` is used which isn't too bad for an option considering
both ido and helm are able to use it for the most basic selection
tasks.

No matter what, helm will stay an "optional" dependency.
2014-09-02 10:15:21 +02:00

472 lines
18 KiB
EmacsLisp

;;; eyebrowse.el --- Easy window config switching -*- lexical-binding: t -*-
;; Copyright (C) 2014 Vasilij Schneidermann <v.schneidermann@gmail.com>
;; Author: Vasilij Schneidermann <v.schneidermann@gmail.com>
;; URL: https://github.com/wasamasa/eyebrowse
;; Version: 0.3
;; Package-Requires: ((dash "2.4.0") (s "1.4.0") (emacs "24"))
;; Keywords: convenience
;; This file is NOT part of GNU Emacs.
;; This file 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 2, or (at your option)
;; any later version.
;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to
;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.
;;; Commentary:
;; This global minor mode provides a set of keybindings for switching
;; window configurations. It tries mimicking the tab behaviour of
;; `ranger`, a file manager.
;; See the README for more info:
;; https://github.com/wasamasa/eyebrowse
;;; Code:
(require 'dash)
(require 's)
;; --- variables -------------------------------------------------------------
(defgroup eyebrowse nil
"A window configuration switcher modeled after the ranger file
manager."
:group 'convenience
:prefix "eyebrowse-")
(defcustom eyebrowse-keymap-prefix (kbd "C-c C-w")
"Prefix key for key-bindings."
:type 'string
:group 'eyebrowse)
(defcustom eyebrowse-lighter " ¬_¬"
"Lighter for `eyebrowse-minor-mode'."
:type 'string
:group 'eyebrowse)
(defface eyebrowse-mode-line-delimiters
'((t (nil)))
"Face for the mode line indicator delimiters."
:group 'eyebrowse)
(defface eyebrowse-mode-line-separator
'((t (nil)))
"Face for the mode line indicator separator."
:group 'eyebrowse)
(defface eyebrowse-mode-line-inactive
'((t (nil)))
"Face for the inactive items of the mode line indicator."
:group 'eyebrowse)
(defface eyebrowse-mode-line-active
'((t (:inherit mode-line-emphasis)))
"Face for the active items of the mode line indicator."
:group 'eyebrowse)
(defcustom eyebrowse-mode-line-separator ", "
"Separator of the mode line indicator."
:type 'string
:group 'eyebrowse)
(defcustom eyebrowse-mode-line-left-delimiter "["
"Left delimiter of the mode line indicator."
:type 'string
:group 'eyebrowse)
(defcustom eyebrowse-mode-line-right-delimiter "]"
"Right delimiter of the mode line indicator."
:type 'string
:group 'eyebrowse)
(defcustom eyebrowse-mode-line-style 'smart
"The mode line indicator style may be one of the following:
'hide: Don't show at all.
'smart: Hide when only one window config.
'always: Always show."
:type '(choice (const :tag "Hide" hide)
(const :tag "Smart" smart)
(const :tag "Always" always))
:group 'eyebrowse)
(defcustom eyebrowse-restore-point-p t
"Restore point, too?
If t, restore point."
:type 'boolean
:group 'eyebrowse)
(defcustom eyebrowse-wrap-around-p nil
"Wrap around when switching to the next/previous window config?
If t, wrap around."
:type 'boolean
:group 'eyebrowse)
(defcustom eyebrowse-switch-back-and-forth-p nil
"Switch to the last window automatically?
If t, switching to the same window config as
`eyebrowse-current-window-config', switches to
`eyebrowse-last-window-config'."
:type 'boolean
:group 'eyebrowse)
(defcustom eyebrowse-pre-window-switch-hook nil
"Hook run before switching to a window config."
:type 'hook
:group 'eyebrowse)
(defcustom eyebrowse-post-window-switch-hook nil
"Hook run after switching to a window config."
:type 'hook
:group 'eyebrowse)
(defvar eyebrowse-mode-map
(let ((map (make-sparse-keymap)))
(let ((prefix-map (make-sparse-keymap)))
(define-key prefix-map (kbd "<") 'eyebrowse-prev-window-config)
(define-key prefix-map (kbd ">") 'eyebrowse-next-window-config)
(define-key prefix-map (kbd "'") 'eyebrowse-last-window-config)
(define-key prefix-map (kbd "\"") 'eyebrowse-close-window-config)
(define-key prefix-map (kbd "0") 'eyebrowse-switch-to-window-config-0)
(define-key prefix-map (kbd "1") 'eyebrowse-switch-to-window-config-1)
(define-key prefix-map (kbd "2") 'eyebrowse-switch-to-window-config-2)
(define-key prefix-map (kbd "3") 'eyebrowse-switch-to-window-config-3)
(define-key prefix-map (kbd "4") 'eyebrowse-switch-to-window-config-4)
(define-key prefix-map (kbd "5") 'eyebrowse-switch-to-window-config-5)
(define-key prefix-map (kbd "6") 'eyebrowse-switch-to-window-config-6)
(define-key prefix-map (kbd "7") 'eyebrowse-switch-to-window-config-7)
(define-key prefix-map (kbd "8") 'eyebrowse-switch-to-window-config-8)
(define-key prefix-map (kbd "9") 'eyebrowse-switch-to-window-config-9)
(define-key map eyebrowse-keymap-prefix prefix-map))
map)
"Initial key map for `eyebrowse-mode'.")
;; --- internal functions ----------------------------------------------------
(defun eyebrowse-get (type &optional frame)
"Retrieve frame-specific value of TYPE.
If FRAME is nil, use current frame. TYPE can be any of
'window-configs, 'current-slot, 'last-slot."
(cond
((eq type 'window-configs)
(frame-parameter frame 'eyebrowse-window-configs))
((eq type 'current-slot)
(frame-parameter frame 'eyebrowse-current-slot))
((eq type 'last-slot)
(frame-parameter frame 'eyebrowse-last-slot))))
(defun eyebrowse-set (type value &optional frame)
"Set frame-specific value of TYPE to VALUE.
If FRAME is nil, use current frame. TYPE can be any of
'window-configs, 'current-slot, 'last-slot."
(cond
((eq type 'window-configs)
(set-frame-parameter frame 'eyebrowse-window-configs value))
((eq type 'current-slot)
(set-frame-parameter frame 'eyebrowse-current-slot value))
((eq type 'last-slot)
(set-frame-parameter frame 'eyebrowse-last-slot value))))
(put 'eyebrowse-set 'lisp-indent-function 1)
(defun eyebrowse-insert-in-window-config-list (element)
"Insert ELEMENT in the list of window configs.
This function keeps the sortedness intact."
(eyebrowse-set 'window-configs
;; TODO there must be a better way to do this with `-insert-at'
;; `-op' would shorten this code if it's good enough as it is
(--sort (< (car it) (car other))
(cons element (eyebrowse-get 'window-configs)))))
(defun eyebrowse-update-window-config-element (old-element new-element)
"Replace OLD-ELEMENT with NEW-ELEMENT in the window config list."
(eyebrowse-set 'window-configs
(-replace-at (-elem-index old-element (eyebrowse-get 'window-configs))
new-element (eyebrowse-get 'window-configs))))
;; window-configs are at the moment a list of a list containing the
;; numerical slot, window configuration and point. To add "tagging",
;; it would be useful to save a tag as fourth component and display it
;; if present, not only in the mode line, but when renaming and
;; selecting a window configuration interactively, too. This
;; obviously requires an interactive window switching command.
;; The display of the tag should be configurable via format string and
;; that format string be able to resemble vim tabs, i3 workspaces,
;; tmux sessions, etc. Therefore it has to contain format codes for
;; slot, tag and buffer name.
(defun eyebrowse-save-window-config (slot)
"Save the current window config to SLOT."
(let* ((element (list slot (current-window-configuration) (point)))
(match (assq slot (eyebrowse-get 'window-configs))))
(if match
(eyebrowse-update-window-config-element match element)
(eyebrowse-insert-in-window-config-list element))))
(defun eyebrowse-load-window-config (slot)
"Restore the window config from SLOT."
(let ((match (assq slot (eyebrowse-get 'window-configs))))
(when match
(let ((window-config (cadr match))
(point (nth 2 match)))
(set-window-configuration window-config)
(goto-char point)))))
(defun eyebrowse-delete-window-config (slot)
"Remove the window config at SLOT."
(let ((window-configs (eyebrowse-get 'window-configs)))
(eyebrowse-set 'window-configs
(remove (assq slot window-configs) window-configs))))
(defun eyebrowse-update-mode-line ()
"Return a string representation of the window configurations."
(let* ((left-delimiter (propertize eyebrowse-mode-line-left-delimiter
'face 'eyebrowse-mode-line-delimiters))
(right-delimiter (propertize eyebrowse-mode-line-right-delimiter
'face 'eyebrowse-mode-line-delimiters))
(separator (propertize eyebrowse-mode-line-separator
'face 'eyebrowse-mode-line-separator))
(current-slot (eyebrowse-get 'current-slot))
(active-item (propertize (number-to-string current-slot)
'face 'eyebrowse-mode-line-active))
(window-configs (eyebrowse-get 'window-configs))
(window-config-slots (mapcar (lambda (item)
(number-to-string (car item)))
window-configs)))
(if (and (not (eq eyebrowse-mode-line-style 'hide))
(or (eq eyebrowse-mode-line-style 'always)
(and (eq eyebrowse-mode-line-style 'smart)
(> (length window-configs) 1))))
(s-concat
left-delimiter
(s-join separator
(-replace-at (-elem-index (number-to-string current-slot)
window-config-slots)
active-item window-config-slots))
right-delimiter)
"")))
(defun eyebrowse-read-slot ()
(let* ((candidates (--map (number-to-string (car it))
(eyebrowse-get 'window-configs)))
(last-slot (number-to-string (eyebrowse-get 'last-slot)))
(selection (completing-read "Enter slot: " candidates
nil nil last-slot))
(slot (string-to-number selection)))
(unless (and (= slot 0) (not (string= selection "0")))
slot)))
;; --- public functions ------------------------------------------------------
(defun eyebrowse-init (&optional frame)
"Initialize Eyebrowse for the current frame."
(eyebrowse-set 'last-slot 1 frame)
(eyebrowse-set 'current-slot 1 frame))
(defun eyebrowse-switch-to-window-config (slot)
"Switch to the window config SLOT.
This will save the current window config to
`eyebrowse-current-slot' first, then switch. If
`eyebrowse-switch-back-and-forth-p' is t and
`eyebrowse-current-slot' equals SLOT, this will switch to the
last window config."
(interactive (list (eyebrowse-read-slot)))
(when slot
(let ((current-slot (eyebrowse-get 'current-slot))
(last-slot (eyebrowse-get 'last-slot)))
(when (and eyebrowse-switch-back-and-forth-p (= current-slot slot))
(setq slot last-slot))
(when (/= current-slot slot)
(run-hooks 'eyebrowse-pre-window-switch-hook)
(eyebrowse-save-window-config current-slot)
(eyebrowse-load-window-config slot)
(eyebrowse-set 'last-slot current-slot)
(eyebrowse-set 'current-slot slot)
(eyebrowse-save-window-config slot)
(eyebrowse-load-window-config slot)
(run-hooks 'eyebrowse-post-window-switch-hook)))))
(defun eyebrowse-next-window-config (count)
"Switch to the next available window config.
If `eyebrowse-wrap-around-p' is t, this will switch from the last
to the first one. When used with a numerical argument, switch to
window config COUNT."
(interactive "P")
(let* ((window-configs (eyebrowse-get 'window-configs))
(match (assq (eyebrowse-get 'current-slot) window-configs))
(index (-elem-index match window-configs)))
(if count
(eyebrowse-switch-to-window-config count)
(when index
(if (< (1+ index) (length window-configs))
(eyebrowse-switch-to-window-config
(car (nth (1+ index) window-configs)))
(when eyebrowse-wrap-around-p
(eyebrowse-switch-to-window-config
(caar window-configs))))))))
(defun eyebrowse-prev-window-config (count)
"Switch to the previous available window config.
If `eyebrowse-wrap-around-p' is t, this will switch from the
first to the last one. When used with a numerical argument,
switch COUNT window configs backwards and always wrap around."
(interactive "P")
(let* ((window-configs (eyebrowse-get 'window-configs))
(match (assq (eyebrowse-get 'current-slot) window-configs))
(index (-elem-index match window-configs)))
(if count
(let ((eyebrowse-wrap-around-p t))
(eyebrowse-prev-window-config
(when (> count 1)
(eyebrowse-prev-window-config (1- count)))))
(when index
(if (> index 0)
(eyebrowse-switch-to-window-config
(car (nth (1- index) window-configs)))
(when eyebrowse-wrap-around-p
(eyebrowse-switch-to-window-config
(caar (last window-configs)))))))))
(defun eyebrowse-last-window-config ()
"Switch to the last window config."
(interactive)
(eyebrowse-switch-to-window-config (eyebrowse-get 'last-slot)))
(defun eyebrowse-close-window-config ()
"Close the current window config.
This removes it from `eyebrowse-window-configs' and switches to
another appropriate window config."
(interactive)
(let ((window-configs (eyebrowse-get 'window-configs)))
(when (> (length window-configs) 1)
(if (equal (assq (eyebrowse-get 'current-slot) window-configs)
(car (last window-configs)))
(eyebrowse-prev-window-config nil)
(eyebrowse-next-window-config nil))
(eyebrowse-delete-window-config (eyebrowse-get 'last-slot)))))
(defun eyebrowse-switch-to-window-config-0 ()
"Switch to window configuration 0."
(interactive)
(eyebrowse-switch-to-window-config 0))
(defun eyebrowse-switch-to-window-config-1 ()
"Switch to window configuration 1."
(interactive)
(eyebrowse-switch-to-window-config 1))
(defun eyebrowse-switch-to-window-config-2 ()
"Switch to window configuration 2."
(interactive)
(eyebrowse-switch-to-window-config 2))
(defun eyebrowse-switch-to-window-config-3 ()
"Switch to window configuration 3."
(interactive)
(eyebrowse-switch-to-window-config 3))
(defun eyebrowse-switch-to-window-config-4 ()
"Switch to window configuration 4."
(interactive)
(eyebrowse-switch-to-window-config 4))
(defun eyebrowse-switch-to-window-config-5 ()
"Switch to window configuration 5."
(interactive)
(eyebrowse-switch-to-window-config 5))
(defun eyebrowse-switch-to-window-config-6 ()
"Switch to window configuration 6."
(interactive)
(eyebrowse-switch-to-window-config 6))
(defun eyebrowse-switch-to-window-config-7 ()
"Switch to window configuration 7."
(interactive)
(eyebrowse-switch-to-window-config 7))
(defun eyebrowse-switch-to-window-config-8 ()
"Switch to window configuration 8."
(interactive)
(eyebrowse-switch-to-window-config 8))
(defun eyebrowse-switch-to-window-config-9 ()
"Switch to window configuration 9."
(interactive)
(eyebrowse-switch-to-window-config 9))
;;;###autoload
(defun eyebrowse-setup-opinionated-keys ()
"Set up more opinionated key bindings for using eyebrowse.
M-1..M-9, C-< / C->, C-`and C-' are used for switching. If evil
is detected, it will bind gt, gT, gc and zx, too."
(let ((map eyebrowse-mode-map))
(define-key map (kbd "C-<") 'eyebrowse-prev-window-config)
(define-key map (kbd "C->") 'eyebrowse-next-window-config)
(define-key map (kbd "C-'") 'eyebrowse-last-window-config)
(define-key map (kbd "C-\"") 'eyebrowse-close-window-config)
(when (and (boundp 'evil-mode) evil-mode)
(define-key evil-motion-state-map (kbd "gt")
'eyebrowse-next-window-config)
(define-key evil-motion-state-map (kbd "gT")
'eyebrowse-prev-window-config)
(define-key evil-motion-state-map (kbd "gc")
'eyebrowse-close-window-config)
(define-key evil-motion-state-map (kbd "zx")
'eyebrowse-last-window-config))
(define-key map (kbd "M-0") 'eyebrowse-switch-to-window-config-0)
(define-key map (kbd "M-1") 'eyebrowse-switch-to-window-config-1)
(define-key map (kbd "M-2") 'eyebrowse-switch-to-window-config-2)
(define-key map (kbd "M-3") 'eyebrowse-switch-to-window-config-3)
(define-key map (kbd "M-4") 'eyebrowse-switch-to-window-config-4)
(define-key map (kbd "M-5") 'eyebrowse-switch-to-window-config-5)
(define-key map (kbd "M-6") 'eyebrowse-switch-to-window-config-6)
(define-key map (kbd "M-7") 'eyebrowse-switch-to-window-config-7)
(define-key map (kbd "M-8") 'eyebrowse-switch-to-window-config-8)
(define-key map (kbd "M-9") 'eyebrowse-switch-to-window-config-9)))
;;;###autoload
(define-minor-mode eyebrowse-mode
"Toggle `eyebrowse-mode'.
This global minor mode provides a set of keybindings for
switching window configurations. It tries mimicking the tab
behaviour of `ranger`, a file manager."
:lighter eyebrowse-lighter
:keymap eyebrowse-mode-map
:global t
;; the `define-minor-mode' macro apparently sets the mode variable
;; first, then runs the associated code, therefore if
;; `eyebrowse-mode' is t, code related to initialization is run
(if eyebrowse-mode
(progn
;; for some reason it's necessary to init both after emacs
;; started and after frame creation to make it work for both
;; emacs and emacsclient
(eyebrowse-init)
(add-hook 'after-make-frame-functions 'eyebrowse-init)
(add-to-list 'mode-line-misc-info
'(:eval (eyebrowse-update-mode-line)) t))
(remove-hook 'after-make-frame-functions 'eyebrowse-init)
(setq mode-line-misc-info
(remove '(:eval (eyebrowse-update-mode-line)) mode-line-misc-info))))
(provide 'eyebrowse)
;;; eyebrowse.el ends here