Moving to Doom Emacs!

This commit is contained in:
Derek Taylor
2019-12-16 20:21:19 -06:00
parent d9f2f456f1
commit d4b4c33550
683 changed files with 51877 additions and 100 deletions

View File

@@ -0,0 +1,11 @@
#+TITLE: :ui deft
[[https://jblevins.org/projects/deft/][Deft]] is a major mode for browsing and filtering notes written in plain text
formats, such as org-mode, markdown, and LaTeX.
To use this module, in your config file set the value of the variable ~deft-directory~ to the folder in which you
want to keep your notes.
The default note format is org-mode. You can change this by setting the value of
the variable ~deft-default-extension~. Changing the value to ~"md"~ for example,
will change the default note format to markdown.

View File

@@ -0,0 +1,32 @@
;;; ui/deft/config.el -*- lexical-binding: t; -*-
(use-package! deft
:commands deft
:init
(setq deft-extensions '("org" "md" "tex" "txt")
deft-default-extension "org"
;; de-couples filename and note title:
deft-use-filename-as-title nil
deft-use-filter-string-for-filename t
deft-org-mode-title-prefix t
;; converts the filter string into a readable file-name using kebab-case:
deft-file-naming-rules
'((noslash . "-")
(nospace . "-")
(case-fn . downcase)))
:config
;; start filtering immediately
(set-evil-initial-state! 'deft-mode 'insert)
(map! :map deft-mode-map
:localleader
"RET" #'deft-new-file-named
"a" #'deft-archive-file
"c" #'deft-filter-clear
"d" #'deft-delete-file
"f" #'deft-find-file
"g" #'deft-refresh
"l" #'deft-filter
"n" #'deft-new-file
"r" #'deft-rename-file
"s" #'deft-toggle-sort-method
"t" #'deft-toggle-incremental-search))

View File

@@ -0,0 +1,4 @@
;; -*- no-byte-compile: t; -*-
;;; ui/deft/packages.el
(package! deft)

View File

@@ -0,0 +1,20 @@
#+TITLE: ui/doom-dashboard
#+DATE: October 9, 2019
#+SINCE: v1.3
#+STARTUP: inlineimages
* Table of Contents :TOC_3:noexport:
- [[#description][Description]]
- [[#module-flags][Module Flags]]
- [[#prerequisites][Prerequisites]]
* Description
This module gives Doom Emacs a dashboard buffer.
It is loosely inspired by Atom's dashboard.
** Module Flags
This module provides no flags.
* Prerequisites
This module only requires that ~all-the-icons~'s icon fonts are installed. Use ~M-x all-the-icons-install-fonts~ to do so.

View File

@@ -0,0 +1,28 @@
;;; ui/doom-dashboard/autoload.el -*- lexical-binding: t; -*-
(defun +doom-dashboard--help-echo ()
(when-let* ((btn (button-at (point)))
(msg (button-get btn 'help-echo)))
(message "%s" msg)))
;;;###autoload
(defun +doom-dashboard/open (frame)
"Switch to the dashboard in the current window, of the current FRAME."
(interactive (list (selected-frame)))
(with-selected-frame frame
(switch-to-buffer (doom-fallback-buffer))
(+doom-dashboard-reload t)))
;;;###autoload
(defun +doom-dashboard/forward-button (n)
"Like `forward-button', but don't wrap."
(interactive "p")
(forward-button n nil)
(+doom-dashboard--help-echo))
;;;###autoload
(defun +doom-dashboard/backward-button (n)
"Like `backward-button', but don't wrap."
(interactive "p")
(backward-button n nil)
(+doom-dashboard--help-echo))

View File

@@ -0,0 +1,455 @@
;;; ui/doom-dashboard/config.el -*- lexical-binding: t; -*-
(defvar +doom-dashboard-name "*doom*"
"The name to use for the dashboard buffer.")
(defvar +doom-dashboard-functions
'(doom-dashboard-widget-banner
doom-dashboard-widget-shortmenu
doom-dashboard-widget-loaded
doom-dashboard-widget-footer)
"List of widget functions to run in the dashboard buffer to construct the
dashboard. These functions take no arguments and the dashboard buffer is current
while they run.")
(defvar +doom-dashboard-banner-file "default.png"
"The path to the image file to be used in on the dashboard. The path is
relative to `+doom-dashboard-banner-dir'. If nil, always use the ASCII banner.")
(defvar +doom-dashboard-banner-dir (concat (dir!) "/banners/")
"Where to look for `+doom-dashboard-banner-file'.")
(defvar +doom-dashboard-banner-padding '(4 . 4)
"Number of newlines to pad the banner with, above and below, respectively.")
(defvar +doom-dashboard-inhibit-refresh nil
"If non-nil, the doom buffer won't be refreshed.")
(defvar +doom-dashboard-inhibit-functions ()
"A list of functions which take no arguments. If any of them return non-nil,
dashboard reloading is inhibited.")
(defvar +doom-dashboard-pwd-policy 'last-project
"The policy to use when setting the `default-directory' in the dashboard.
Possible values:
'last-project the `doom-project-root' of the last open buffer
'last the `default-directory' of the last open buffer
a FUNCTION a function run with the `default-directory' of the last
open buffer, that returns a directory path
a STRING a fixed path
nil `default-directory' will never change")
(defvar +doom-dashboard-menu-sections
'(("Reload last session"
:icon (all-the-icons-octicon "history" :face 'font-lock-keyword-face)
:when (cond ((require 'persp-mode nil t)
(file-exists-p (expand-file-name persp-auto-save-fname persp-save-dir)))
((require 'desktop nil t)
(file-exists-p (desktop-full-file-name))))
:face (:inherit (font-lock-keyword-face bold))
:action doom/quickload-session)
("Open org-agenda"
:icon (all-the-icons-octicon "calendar" :face 'font-lock-keyword-face)
:when (fboundp 'org-agenda)
:action org-agenda)
("Recently opened files"
:icon (all-the-icons-octicon "file-text" :face 'font-lock-keyword-face)
:action recentf-open-files)
("Open project"
:icon (all-the-icons-octicon "briefcase" :face 'font-lock-keyword-face)
:action projectile-switch-project)
("Jump to bookmark"
:icon (all-the-icons-octicon "bookmark" :face 'font-lock-keyword-face)
:action bookmark-jump)
("Open private configuration"
:icon (all-the-icons-octicon "tools" :face 'font-lock-keyword-face)
:when (file-directory-p doom-private-dir)
:action doom/open-private-config)
("Search Documentation"
:icon (all-the-icons-octicon "book" :face 'font-lock-keyword-face)
:action doom/help-search))
"An alist of menu buttons used by `doom-dashboard-widget-shortmenu'. Each
element is a cons cell (LABEL . PLIST). LABEL is a string to display after the
icon and before the key string.
PLIST can have the following properties:
:icon FORM
Uses the return value of FORM as an icon (can be literal string).
:key STRING
The keybind displayed next to the button.
:when FORM
If FORM returns nil, don't display this button.
:face FACE
Displays the icon and text with FACE (a face symbol).
:action FORM
Run FORM when the button is pushed.")
;;
(defvar +doom-dashboard--last-cwd nil)
(defvar +doom-dashboard--width 80)
(defvar +doom-dashboard--old-fringe-indicator fringe-indicator-alist)
(defvar +doom-dashboard--pwd-alist ())
(defvar +doom-dashboard--reload-timer nil)
(defvar all-the-icons-scale-factor)
(defvar all-the-icons-default-adjust)
;;
;;; Bootstrap
(defun +doom-dashboard-init-h ()
"Initializes Doom's dashboard."
(unless noninteractive
;; Ensure the dashboard becomes Emacs' go-to buffer when there's nothing
;; else to show.
(setq doom-fallback-buffer-name +doom-dashboard-name
initial-buffer-choice #'doom-fallback-buffer)
(unless fancy-splash-image
(setq fancy-splash-image
(expand-file-name +doom-dashboard-banner-file
+doom-dashboard-banner-dir)))
(when (equal (buffer-name) "*scratch*")
(set-window-buffer nil (doom-fallback-buffer))
(if (daemonp)
(add-hook 'after-make-frame-functions #'+doom-dashboard-reload-frame-h)
(+doom-dashboard-reload)))
;; Ensure the dashboard is up-to-date whenever it is switched to or resized.
(add-hook 'window-configuration-change-hook #'+doom-dashboard-resize-h)
(add-hook 'window-size-change-functions #'+doom-dashboard-resize-h)
(add-hook 'doom-switch-buffer-hook #'+doom-dashboard-reload-maybe-h)
(add-hook 'delete-frame-functions #'+doom-dashboard-reload-frame-h)
;; `persp-mode' integration: update `default-directory' when switching perspectives
(add-hook 'persp-created-functions #'+doom-dashboard--persp-record-project-h)
(add-hook 'persp-activated-functions #'+doom-dashboard--persp-detect-project-h)
(add-hook 'persp-before-switch-functions #'+doom-dashboard--persp-record-project-h)))
(add-hook 'doom-init-ui-hook #'+doom-dashboard-init-h)
;;
;;; Major mode
(define-derived-mode +doom-dashboard-mode special-mode
(format "DOOM v%s" doom-version)
"Major mode for the DOOM dashboard buffer."
:syntax-table nil
:abbrev-table nil
(buffer-disable-undo)
(setq truncate-lines t)
(setq-local whitespace-style nil)
(setq-local show-trailing-whitespace nil)
(setq-local hscroll-margin 0)
(setq-local tab-width 2)
;; Don't scroll to follow cursor
(setq-local scroll-preserve-screen-position nil)
(setq-local auto-hscroll-mode nil)
(cl-loop for (car . _cdr) in fringe-indicator-alist
collect (cons car nil) into alist
finally do (setq fringe-indicator-alist alist))
;; Ensure point is always on a button
(add-hook 'post-command-hook #'+doom-dashboard-reposition-point-h nil t))
(define-key! +doom-dashboard-mode-map
[left-margin mouse-1] #'ignore
[remap forward-button] #'+doom-dashboard/forward-button
[remap backward-button] #'+doom-dashboard/backward-button
"n" #'forward-button
"p" #'backward-button
"C-n" #'forward-button
"C-p" #'backward-button
[down] #'forward-button
[up] #'backward-button
[tab] #'forward-button
[backtab] #'backward-button
;; Evil remaps
[remap evil-next-line] #'forward-button
[remap evil-previous-line] #'backward-button
[remap evil-next-visual-line] #'forward-button
[remap evil-previous-visual-line] #'backward-button
[remap evil-paste-pop-next] #'forward-button
[remap evil-paste-pop] #'backward-button
[remap evil-delete] #'ignore
[remap evil-delete-line] #'ignore
[remap evil-insert] #'ignore
[remap evil-append] #'ignore
[remap evil-replace] #'ignore
[remap evil-replace-state] #'ignore
[remap evil-change] #'ignore
[remap evil-change-line] #'ignore
[remap evil-visual-char] #'ignore
[remap evil-visual-line] #'ignore)
;;
;;; Hooks
(defun +doom-dashboard-reposition-point-h ()
"Trap the point in the buttons."
(when (region-active-p)
(setq deactivate-mark t)
(when (bound-and-true-p evil-local-mode)
(evil-change-to-previous-state)))
(or (ignore-errors
(if (button-at (point))
(forward-button 0)
(backward-button 1)))
(ignore-errors
(goto-char (point-min))
(forward-button 1))))
(defun +doom-dashboard-reload-maybe-h ()
"Reload the dashboard or its state.
If this isn't a dashboard buffer, move along, but record its `default-directory'
if the buffer is real. See `doom-real-buffer-p' for an explanation for what
'real' means.
If this is the dashboard buffer, reload it completely."
(cond ((+doom-dashboard-p (current-buffer))
(let (+doom-dashboard-inhibit-refresh)
(ignore-errors (+doom-dashboard-reload))))
((and (not (file-remote-p default-directory))
(doom-real-buffer-p (current-buffer)))
(setq +doom-dashboard--last-cwd default-directory)
(+doom-dashboard-update-pwd))))
(defun +doom-dashboard-reload-frame-h (_frame)
"Reload the dashboard after a brief pause. This is necessary for new frames,
whose dimensions may not be fully initialized by the time this is run."
(when (timerp +doom-dashboard--reload-timer)
(cancel-timer +doom-dashboard--reload-timer)) ; in case this function is run rapidly
(setq +doom-dashboard--reload-timer (run-with-timer 0.1 nil #'+doom-dashboard-reload t)))
(defun +doom-dashboard-resize-h (&rest _)
"Recenter the dashboard, and reset its margins and fringes."
(let (buffer-list-update-hook
window-configuration-change-hook
window-size-change-functions)
(let ((windows (get-buffer-window-list (doom-fallback-buffer) nil t)))
(dolist (win windows)
(set-window-start win 0)
(set-window-fringes win 0 0)
(set-window-margins
win (max 0 (/ (- (window-total-width win) +doom-dashboard--width) 2))))
(when windows
(with-current-buffer (doom-fallback-buffer)
(save-excursion
(with-silent-modifications
(goto-char (point-min))
(delete-region (line-beginning-position)
(save-excursion (skip-chars-forward "\n")
(point)))
(insert (make-string
(max 0 (- (/ (window-height (get-buffer-window)) 2)
(round (/ (+ (count-lines (point-min) (point-max))
(car +doom-dashboard-banner-padding))
2))))
?\n)))))))))
(defun +doom-dashboard--persp-detect-project-h (&rest _)
"Check for a `last-project-root' parameter in the perspective, and set the
dashboard's `default-directory' to it if it exists.
This and `+doom-dashboard--persp-record-project-h' provides `persp-mode' integration with
the Doom dashboard. It ensures that the dashboard is always in the correct
project (which may be different across perspective)."
(when (bound-and-true-p persp-mode)
(when-let (pwd (persp-parameter 'last-project-root))
(+doom-dashboard-update-pwd pwd))))
(defun +doom-dashboard--persp-record-project-h (&optional persp &rest _)
"Record the last `doom-project-root' for the current perspective. See
`+doom-dashboard--persp-detect-project-h' for more information."
(when (bound-and-true-p persp-mode)
(set-persp-parameter
'last-project-root (doom-project-root)
(if (persp-p persp)
persp
(get-current-persp)))))
;;
;;; Library
(defun +doom-dashboard-p (buffer)
"Returns t if BUFFER is the dashboard buffer."
(eq buffer (get-buffer +doom-dashboard-name)))
(defun +doom-dashboard-update-pwd (&optional pwd)
"Update `default-directory' in the Doom dashboard buffer. What it is set to is
controlled by `+doom-dashboard-pwd-policy'."
(if pwd
(with-current-buffer (doom-fallback-buffer)
(doom-log "Changed dashboard's PWD to %s" pwd)
(setq-local default-directory pwd))
(let ((new-pwd (+doom-dashboard--get-pwd)))
(when (and new-pwd (file-accessible-directory-p new-pwd))
(+doom-dashboard-update-pwd
(concat (directory-file-name new-pwd)
"/"))))))
(defun +doom-dashboard-reload (&optional force)
"Update the DOOM scratch buffer (or create it, if it doesn't exist)."
(when (or (and (not +doom-dashboard-inhibit-refresh)
(get-buffer-window (doom-fallback-buffer))
(not (window-minibuffer-p (frame-selected-window)))
(not (run-hook-with-args-until-success '+doom-dashboard-inhibit-functions)))
force)
(with-current-buffer (doom-fallback-buffer)
(doom-log "Reloading dashboard at %s" (format-time-string "%T"))
(with-silent-modifications
(let ((pt (point)))
(unless (eq major-mode '+doom-dashboard-mode)
(+doom-dashboard-mode))
(erase-buffer)
(run-hooks '+doom-dashboard-functions)
(goto-char pt)
(+doom-dashboard-reposition-point-h))
(+doom-dashboard-resize-h)
(+doom-dashboard--persp-detect-project-h)
(+doom-dashboard-update-pwd)
(current-buffer)))))
;; helpers
(defun +doom-dashboard--center (len s)
(concat (make-string (ceiling (max 0 (- len (length s))) 2) ? )
s))
(defun +doom-dashboard--get-pwd ()
(let ((lastcwd +doom-dashboard--last-cwd)
(policy +doom-dashboard-pwd-policy))
(cond ((null policy)
default-directory)
((stringp policy)
(expand-file-name policy lastcwd))
((functionp policy)
(funcall policy lastcwd))
((null lastcwd)
default-directory)
((eq policy 'last-project)
(let ((cwd default-directory))
(or (doom-project-root lastcwd)
cwd)))
((eq policy 'last)
lastcwd)
((warn "`+doom-dashboard-pwd-policy' has an invalid value of '%s'"
policy)))))
;;
;;; Widgets
(defun doom-dashboard-widget-banner ()
(let ((point (point)))
(mapc (lambda (line)
(insert (propertize (+doom-dashboard--center +doom-dashboard--width line)
'face 'font-lock-comment-face) " ")
(insert "\n"))
'("================= =============== =============== ======== ========"
"\\\\ . . . . . . .\\\\ //. . . . . . .\\\\ //. . . . . . .\\\\ \\\\. . .\\\\// . . //"
"||. . ._____. . .|| ||. . ._____. . .|| ||. . ._____. . .|| || . . .\\/ . . .||"
"|| . .|| ||. . || || . .|| ||. . || || . .|| ||. . || ||. . . . . . . ||"
"||. . || || . .|| ||. . || || . .|| ||. . || || . .|| || . | . . . . .||"
"|| . .|| ||. _-|| ||-_ .|| ||. . || || . .|| ||. _-|| ||-_.|\\ . . . . ||"
"||. . || ||-' || || `-|| || . .|| ||. . || ||-' || || `|\\_ . .|. .||"
"|| . _|| || || || || ||_ . || || . _|| || || || |\\ `-_/| . ||"
"||_-' || .|/ || || \\|. || `-_|| ||_-' || .|/ || || | \\ / |-_.||"
"|| ||_-' || || `-_|| || || ||_-' || || | \\ / | `||"
"|| `' || || `' || || `' || || | \\ / | ||"
"|| .===' `===. .==='.`===. .===' /==. | \\/ | ||"
"|| .==' \\_|-_ `===. .===' _|_ `===. .===' _-|/ `== \\/ | ||"
"|| .==' _-' `-_ `=' _-' `-_ `=' _-' `-_ /| \\/ | ||"
"|| .==' _-' '-__\\._-' '-_./__-' `' |. /| | ||"
"||.==' _-' `' | /==.||"
"==' _-' E M A C S \\/ `=="
"\\ _-' `-_ /"
" `'' ``'"))
(when (and (display-graphic-p)
(stringp fancy-splash-image)
(file-readable-p fancy-splash-image))
(let ((image (create-image (fancy-splash-image-file))))
(add-text-properties
point (point) `(display ,image rear-nonsticky (display)))
(save-excursion
(goto-char point)
(insert (make-string
(truncate
(max 0 (+ 1 (/ (- +doom-dashboard--width
(car (image-size image nil)))
2))))
? ))))
(insert (make-string (or (cdr +doom-dashboard-banner-padding) 0)
?\n)))))
(defun doom-dashboard-widget-loaded ()
(insert
"\n\n"
(propertize
(+doom-dashboard--center
+doom-dashboard--width
(doom-display-benchmark-h 'return))
'face 'font-lock-comment-face)
"\n"))
(defun doom-dashboard-widget-shortmenu ()
(let ((all-the-icons-scale-factor 1.45)
(all-the-icons-default-adjust -0.02))
(insert "\n")
(dolist (section +doom-dashboard-menu-sections)
(cl-destructuring-bind (label &key icon action when face) section
(when (and (fboundp action)
(or (null when)
(eval when t)))
(insert
(+doom-dashboard--center
(- +doom-dashboard--width 1)
(let ((icon (if (stringp icon) icon (eval icon t))))
(format (format "%s%%s%%-10s" (if icon "%3s\t" "%3s"))
(or icon "")
(with-temp-buffer
(insert-text-button
label
'action
`(lambda (_)
(call-interactively (or (command-remapping #',action)
#',action)))
'face (or face 'font-lock-keyword-face)
'follow-link t
'help-echo
(format "%s (%s)" label
(propertize (symbol-name action) 'face 'font-lock-constant-face)))
(format "%-37s" (buffer-string)))
;; Lookup command keys dynamically
(or (when-let (key (where-is-internal action nil t))
(with-temp-buffer
(save-excursion (insert (key-description key)))
(while (re-search-forward "<\\([^>]+\\)>" nil t)
(let ((str (match-string 1)))
(replace-match
(upcase (if (< (length str) 3)
str
(substring str 0 3))))))
(propertize (buffer-string) 'face 'font-lock-constant-face)))
""))))
(if (display-graphic-p)
"\n\n"
"\n")))))))
(defun doom-dashboard-widget-footer ()
(insert
"\n"
(+doom-dashboard--center
(- +doom-dashboard--width 2)
(with-temp-buffer
(insert-text-button (or (all-the-icons-octicon "octoface" :face 'all-the-icons-green :height 1.3 :v-adjust -0.15)
(propertize "github" 'face 'font-lock-keyword-face))
'action (lambda (_) (browse-url "https://github.com/hlissner/doom-emacs"))
'follow-link t
'help-echo "Open Doom Emacs github page")
(buffer-string)))
"\n"))

View File

@@ -0,0 +1,40 @@
;; -*- no-byte-compile: t; -*-
;;; ui/doom-dashboard/test/test-doom-dashboard.el
(require 'core-projects)
(require 'projectile)
(require! :ui doom-dashboard)
(describe "ui/doom-dashboard"
:var (default-directory projectile-enable-caching)
(before-all
(setq projectile-enable-caching nil
doom-fallback-buffer-name +doom-dashboard-name))
(before-each (projectile-mode +1))
(after-each (projectile-mode -1))
(describe "get-pwd"
:var (+doom-dashboard--last-cwd)
(before-each
(setq +doom-dashboard--last-cwd doom-core-dir
default-directory doom-core-dir))
(it "returns the current directory when policy is nil"
(let (+doom-dashboard-pwd-policy)
(expect (+doom-dashboard--get-pwd) :to-equal default-directory)))
(it "returns a path if policy is a path"
(let ((+doom-dashboard-pwd-policy "~"))
(expect (+doom-dashboard--get-pwd) :to-equal (expand-file-name "~"))))
(it "returns return value of policy as a function"
(let ((+doom-dashboard-pwd-policy (lambda (x) "x")))
(expect (+doom-dashboard--get-pwd) :to-equal "x")))
(it "returns last cwd if policy is 'last"
(let ((+doom-dashboard-pwd-policy 'last))
(expect (+doom-dashboard--get-pwd) :to-equal doom-core-dir)))
(it "returns last project if policy is 'last-project"
(let ((+doom-dashboard-pwd-policy 'last-project))
(expect (+doom-dashboard--get-pwd) :to-equal doom-emacs-dir))))
(describe "dashboard-p"
(it "changes the fallback buffer to the dashboard buffer"
(expect (+doom-dashboard-p (doom-fallback-buffer))))))

View File

@@ -0,0 +1,8 @@
#+TITLE: ui/doom-quit
#+DATE: February 19, 2017
#+SINCE: v2.0
#+STARTUP: inlineimages
A silly module that prompts you with messages when you try to quit, like DOOM
did. Some quotes are from Doom's quit-message list. Others are random, nerdy
references that no decent human being has any business recognizing.

View File

@@ -0,0 +1,32 @@
;;; ui/doom-quit/config.el -*- lexical-binding: t; -*-
(defvar +doom-quit-messages
'(;; from Doom 1
"Please don't leave, there's more demons to toast!"
"Let's beat it -- This is turning into a bloodbath!"
"I wouldn't leave if I were you. DOS is much worse."
"Don't leave yet -- There's a demon around that corner!"
"Ya know, next time you come in here I'm gonna toast ya."
"Go ahead and leave. See if I care."
"Are you sure you want to quit this great editor?"
;; Custom
"(setq nothing t everything 'permitted)"
"Emacs will remember that."
"Emacs, Emacs never changes."
"Hey! Hey, M-x listen!"
"I'm the man who's going to burn your house down! With lemons!"
"It's not like I'll miss you or anything, b-baka!"
"Okay, look. We've both said a lot of things you're going to regret..."
"Wake up, Mr. Stallman. Wake up and smell the ashes."
"You are *not* prepared!")
"A list of quit messages, picked randomly by `+doom-quit'. Taken from
http://doom.wikia.com/wiki/Quit_messages and elsewhere.")
(defun +doom-quit-fn (&rest _)
(doom-quit-p
(format "%s Quit?"
(nth (random (length +doom-quit-messages))
+doom-quit-messages))))
;;
(setq confirm-kill-emacs #'+doom-quit-fn)

View File

@@ -0,0 +1,93 @@
#+TITLE: ui/doom
#+DATE: October 9, 2019
#+SINCE: v1.3
#+STARTUP: inlineimages
* Table of Contents :TOC_3:noexport:
- [[#description][Description]]
- [[#module-flags][Module Flags]]
- [[#plugins][Plugins]]
- [[#prerequisites][Prerequisites]]
- [[#configuration][Configuration]]
- [[#changing-theme][Changing theme]]
- [[#changing-fonts][Changing fonts]]
- [[#troubleshooting][Troubleshooting]]
- [[#strange-font-symbols][Strange font symbols]]
- [[#ugly-background-colors-in-tty-emacs-for-daemon-users][Ugly background colors in tty Emacs for daemon users]]
* Description
This module gives Doom its signature look: powered by the =doom-one= theme
(inspired by Atom's One Dark theme) and =solaire-mode=.
+ A colorscheme inspired by Atom's One Dark theme (now available in a separate
plugin: [[https://github.com/hlissner/emacs-doom-theme/][doom-themes]])
+ A custom folded-region indicator for ~hideshow~
+ "Thin bar" fringe bitmaps for ~git-gutter-fringe~
+ File-visiting buffers are slightly brighter (thanks to solaire-mode)
** Module Flags
This module provides no flags.
** Plugins
+ [[https://github.com/hlissner/emacs-doom-themes][doom-themes]]
+ [[https://github.com/hlissner/emacs-solaire-mode][solaire-mode]]
* Prerequisites
This module has no prereqisites.
* Configuration
** Changing theme
Although this module uses the ~doom-one~ theme by default, [[https://github.com/hlissner/emacs-doom-theme/][doom-themes]] offers a number of alternatives:
+ *doom-one:* doom-themes' flagship theme, inspired by [[https://atom.io/][Atom's]] One Dark themes
+ *doom-vibrant:* a more vibrant version of doom-one
+ *doom-molokai:* based on Textmate's monokai
+ *doom-nova:* adapted from [[https://trevordmiller.com/projects/nova][Nova]]
+ *doom-one-light:* light version of doom-one
+ *doom-peacock:* based on Peacock from [[https://daylerees.github.io/][daylerees' themes]]
+ *doom-tomorrow-night:* by [[https://github.com/ChrisKempson/Tomorrow-Theme][Chris Kempson]]
This can be changed by changing the ~doom-theme~ variable, e.g.
#+BEGIN_SRC emacs-lisp
(setq doom-theme 'doom-molokai)
#+END_SRC
** Changing fonts
core/core-ui.el has four relevant variables:
+ ~doom-font~ :: the default font to use in Doom Emacs.
+ ~doom-big-font~ :: the font to use when ~doom-big-font-mode~ is enabled.
+ ~doom-variable-font~ :: the font to use when ~variable-pitch-mode~ is active (or where the ~variable-pitch~ face is used).
+ ~doom-unicode-font~ :: the font used to display unicode symbols. This is ignored if the =:ui unicode= module is enabled.
#+BEGIN_SRC emacs-lisp
(setq doom-font (font-spec :family "Fira Mono" :size 12)
doom-variable-pitch-font (font-spec :family "Fira Sans")
doom-unicode-font (font-spec :family "DejaVu Sans Mono")
doom-big-font (font-spec :family "Fira Mono" :size 19))
#+END_SRC
* Troubleshooting
** Strange font symbols
If you're seeing strange unicode symbols, this is likely because you don't have
~all-the-icons~'s font icon installed. You can install them with ~M-x
all-the-icons-install-fonts~.
** Ugly background colors in tty Emacs for daemon users
=solaire-mode= is an aesthetic plugin that makes file-visiting buffers brighter
than the rest of the Emacs' frame (to visually differentiate temporary windows
or sidebars from editing windows). This looks great in GUI Emacs, but can look
questionable in the terminal.
It disables itself if you start tty Emacs with ~emacs -nw~, but if you create a
tty frame from a daemon (which solaire-mode cannot anticipate), you'll get an
ugly background instead.
If you only use Emacs in the terminal, your best bet is to disable the
solaire-mode package:
#+BEGIN_SRC elisp
;; in ~/.doom.d/packages.el
(package! solaire-mode :disable t)
#+END_SRC

View File

@@ -0,0 +1,96 @@
;;; ui/doom/config.el -*- lexical-binding: t; -*-
(defvar +doom-solaire-themes
'((doom-challenger-deep . t)
(doom-city-lights . t)
(doom-dracula . t)
(doom-molokai)
(doom-nord . t)
(doom-nord-light . t)
(doom-nova)
(doom-one . t)
(doom-one-light . t)
(doom-opera . t)
(doom-snazzy . t)
(doom-solarized-dark . t)
(doom-solarized-light)
(doom-spacegrey . t)
(doom-tomorrow-day . t)
(doom-tomorrow-night . t)
(doom-vibrant))
"An alist of themes that support `solaire-mode'. If CDR is t, then
`solaire-mode-swap-bg' will be used automatically, when the theme is loaded.")
;;
;;; Packages
(use-package! doom-themes
:defer t
:init
(unless doom-theme
(setq doom-theme 'doom-one))
:config
;; improve integration w/ org-mode
(add-hook 'doom-load-theme-hook #'doom-themes-org-config)
;; more Atom-esque file icons for neotree/treemacs
(when (featurep! :ui neotree)
(add-hook 'doom-load-theme-hook #'doom-themes-neotree-config)
(setq doom-themes-neotree-enable-variable-pitch t
doom-themes-neotree-file-icons 'simple
doom-themes-neotree-line-spacing 2))
(when (featurep! :ui treemacs)
(add-hook 'doom-load-theme-hook #'doom-themes-treemacs-config)))
(use-package! solaire-mode
:when (or (daemonp) (display-graphic-p))
:defer t
:init
(add-hook! 'doom-load-theme-hook :append
(defun +doom-solaire-mode-swap-bg-maybe-h ()
(pcase-let ((`(,_theme . ,swap) (assq doom-theme +doom-solaire-themes)))
(require 'solaire-mode)
(if swap (solaire-mode-swap-bg)))))
:config
;; fringe can become unstyled when deleting or focusing frames
(add-hook 'focus-in-hook #'solaire-mode-reset)
;; Prevent color glitches when reloading either DOOM or loading a new theme
(add-hook! '(doom-load-theme-hook doom-reload-hook) :append
#'solaire-mode-reset)
;; org-capture takes an org buffer and narrows it. The result is erroneously
;; considered an unreal buffer, so solaire-mode must be restored.
(add-hook 'org-capture-mode-hook #'turn-on-solaire-mode)
;; On Emacs 26+, when point is on the last line and solaire-mode is remapping
;; the hl-line face, hl-line's highlight bleeds into the rest of the window
;; after eob. On Emacs 27 this no longer happens.
(unless EMACS27+
(defun +doom--line-range-fn ()
(cons (line-beginning-position)
(cond ((let ((eol (line-end-position)))
(and (= eol (point-max))
(/= eol (line-beginning-position))))
(1- (line-end-position)))
((or (eobp)
(= (line-end-position 2) (point-max)))
(line-end-position))
((line-beginning-position 2)))))
(setq hl-line-range-function #'+doom--line-range-fn))
;; Because fringes can't be given a buffer-local face, they can look odd, so
;; we remove them in the minibuffer and which-key popups (they serve no
;; purpose there anyway).
(add-hook! 'solaire-mode-hook
(defun +doom-disable-fringes-in-minibuffer-h (&rest _)
(set-window-fringes (minibuffer-window) 0 0 nil)))
(defadvice! +doom--no-fringes-in-which-key-buffer-a (&rest _)
:after 'which-key--show-buffer-side-window
(+doom-disable-fringes-in-minibuffer-h)
(set-window-fringes (get-buffer-window which-key--buffer) 0 0 nil))
(add-hook! '(minibuffer-setup-hook window-configuration-change-hook)
#'+doom-disable-fringes-in-minibuffer-h)
(solaire-global-mode +1))

View File

@@ -0,0 +1,6 @@
;; -*- no-byte-compile: t; -*-
;;; ui/doom/packages.el
(package! doom-themes)
(package! solaire-mode)

View File

@@ -0,0 +1,7 @@
;;; ui/fill-column/autoload.el -*- lexical-binding: t; -*-
;;;###autoload (autoload 'hl-fill-column-mode "hl-fill-column" nil t)
;;;###autoload
(add-hook! '(text-mode-hook prog-mode-hook conf-mode-hook)
#'hl-fill-column-mode)

View File

@@ -0,0 +1,4 @@
;; -*- no-byte-compile: t; -*-
;;; ui/fill-column/packages.el
(package! hl-fill-column)

View File

@@ -0,0 +1,13 @@
#+TITLE: :ui hl-todo
This module adds syntax highlighting for TODO/FIXME/NOTE tags in programming major-modes.
What keywords are highlighted (and their color) can be customized through ~hl-todo-keyword-faces~.
#+BEGIN_SRC emacs-lisp
;; the default
(setq hl-todo-keyword-faces
`(("TODO" . ,(face-foreground 'warning))
("FIXME" . ,(face-foreground 'error))
("NOTE" . ,(face-foreground 'success))))
#+END_SRC

View File

@@ -0,0 +1,28 @@
;;; ui/hl-todo/packages.el -*- lexical-binding: t; -*-
(use-package! hl-todo
:hook (prog-mode . hl-todo-mode)
:config
(setq hl-todo-highlight-punctuation ":"
hl-todo-keyword-faces
`(("TODO" warning bold)
("FIXME" error bold)
("HACK" font-lock-constant-face bold)
("REVIEW" font-lock-keyword-face bold)
("NOTE" success bold)
("DEPRECATED" font-lock-doc-face bold)))
;; Use a more primitive todo-keyword detection method in major modes that
;; don't use/have a valid syntax table entry for comments.
(add-hook! '(pug-mode-hook haml-mode-hook)
(defun +hl-todo--use-face-detection-h ()
"Use a different, more primitive method of locating todo keywords."
(set (make-local-variable 'hl-todo-keywords)
'(((lambda (limit)
(let (case-fold-search)
(and (re-search-forward hl-todo-regexp limit t)
(memq 'font-lock-comment-face (doom-enlist (get-text-property (point) 'face))))))
(1 (hl-todo-get-face) t t))))
(when hl-todo-mode
(hl-todo-mode -1)
(hl-todo-mode +1)))))

View File

@@ -0,0 +1,4 @@
;; -*- no-byte-compile: t; -*-
;;; ui/hl-todo/packages.el
(package! hl-todo)

View File

@@ -0,0 +1,31 @@
#+TITLE: ui/hydra
#+DATE: october 29, 2019
#+SINCE: 2.0
#+STARTUP: inlineimages
* Table of Contents :TOC_3:noexport:
- [[#description][Description]]
- [[#module-flags][Module Flags]]
- [[#plugins][Plugins]]
- [[#prerequisites][Prerequisites]]
- [[#configuration][Configuration]]
* Description
This module adds hydra to Doom Emacs, as well as a few custom built hydras to
start with :
+ A hydra to control windows ~+hydra/window-nav/body~
+ A hydra to control text zoom level ~+hydra/text-zoom/body~
** Module Flags
This module provides no flags.
** Plugins
+ [[https://github.com/abo-abo/hydra][hydra]]
* Prerequisites
This module has no prereqisites.
* Configuration
Configuring this module is only setting bindings to the provided hydra, or
creating your own ones !

View File

@@ -0,0 +1,47 @@
;;; ui/hydra/autoload/windows.el -*- lexical-binding: t; -*-
;;;###autoload (autoload '+hydra/text-zoom/body "ui/hydra/autoload/windows" nil t)
(defhydra +hydra/text-zoom (:hint t :color red)
"
Text zoom: _j_:zoom in, _k_:zoom out, _0_:reset
"
("j" doom/increase-font-size "in")
("k" doom/decrease-font-size "out")
("0" doom/reset-font-size "reset"))
;;;###autoload (autoload '+hydra/window-nav/body "ui/hydra/autoload/windows" nil t)
(defhydra +hydra/window-nav (:hint nil)
"
Split: _v_ert _s_:horz
Delete: _c_lose _o_nly
Switch Window: _h_:left _j_:down _k_:up _l_:right
Buffers: _p_revious _n_ext _b_:select _f_ind-file
Resize: _H_:splitter left _J_:splitter down _K_:splitter up _L_:splitter right
Move: _a_:up _z_:down _i_menu
"
("z" scroll-up-line)
("a" scroll-down-line)
("i" idomenu)
("h" windmove-left)
("j" windmove-down)
("k" windmove-up)
("l" windmove-right)
("p" previous-buffer)
("n" next-buffer)
("b" switch-to-buffer)
("f" find-file)
("s" split-window-below)
("v" split-window-right)
("c" delete-window)
("o" delete-other-windows)
("H" hydra-move-splitter-left)
("J" hydra-move-splitter-down)
("K" hydra-move-splitter-up)
("L" hydra-move-splitter-right)
("q" nil))

View File

@@ -0,0 +1,15 @@
;;; ui/hydra/config.el -*- lexical-binding: t; -*-
(use-package! hydra-examples
:commands (hydra-move-splitter-up
hydra-move-splitter-down
hydra-move-splitter-right
hydra-move-splitter-left))
;;;###package hydra
(setq lv-use-separator t)
(defadvice! +hydra--inhibit-window-switch-hooks-a (orig-fn)
:around #'lv-window
(let ((doom-inhibit-switch-window-hooks t))
(funcall orig-fn)))

View File

@@ -0,0 +1,4 @@
;; -*- no-byte-compile: t; -*-
;;; ui/hydra/packages.el
(package! hydra)

View File

@@ -0,0 +1,13 @@
;;; ui/indent-guides/config.el -*- lexical-binding: t; -*-
(use-package! highlight-indent-guides
:hook ((prog-mode text-mode conf-mode) . highlight-indent-guides-mode)
:init
(setq highlight-indent-guides-method 'character)
:config
(add-hook 'focus-in-hook #'highlight-indent-guides-auto-set-faces)
;; `highlight-indent-guides' breaks in these modes
(add-hook! '(visual-line-mode-hook org-indent-mode-hook)
(defun +indent-guides-disable-maybe-h ()
(when highlight-indent-guides-mode
(highlight-indent-guides-mode -1)))))

View File

@@ -0,0 +1,4 @@
;; -*- no-byte-compile: t; -*-
;;; ui/indent-guides/packages.el
(package! highlight-indent-guides)

View File

@@ -0,0 +1,558 @@
;;; ui/modeline/+default.el -*- lexical-binding: t; -*-
;; This is a slimmed down version of `doom-modeline' that manipulates
;; `mode-line-format' directly. Its purpose is to be truer to the original goal
;; of Doom's modeline: to be more performant and minimalistic alternative to
;; other modeline packages and to be abstraction-light. Too much abstraction is
;; too much magic.
;;
;; Warning: this is still a WIP!
(defun +modeline--set-var-and-refresh-bars-fn (&optional symbol value)
(when symbol
(set-default symbol value))
(when doom-init-time
(+modeline-refresh-bars-h)))
;;
;;; Variables
(defcustom +modeline-height 31
"The height of the modeline.
This is enforced by the xpm bitmap bar in `+modeline-bar'. Without it (and in
the terminal), this variable does nothing.
Use `setq!' to adjust this variable live, as it will trigger an refresh of the
bars in the modeline. `setq' will not."
:type 'integer
:set #'+modeline--set-var-and-refresh-bars-fn)
(defcustom +modeline-bar-width 3
"The width of the bar in the modeline.
If nil, the bar will be made transparent and 1 pixel wide, as to be invisible,
but without sacrificing its ability to enforce `+modeline-height'.
Use `setq!' to adjust this variable live, as it will trigger an refresh of the
bars in the modeline. `setq' will not."
:type 'integer
:set #'+modeline--set-var-and-refresh-bars-fn)
(defvar +modeline-format-alist ()
"An alist of modeline formats defined with `def-modeline!'.
Each entry's CAR is the name and CDR is a cons cell whose CAR is the left-hand
side of the modeline, and whose CDR is the right-hand side.")
;;
;;; Faces
(defface +modeline-bar '((t (:inherit highlight)))
"Face used for left-most bar on the mode-line of an active window.")
(defface +modeline-bar-inactive '((t (:inherit mode-line-inactive)))
"Face used for left-most bar on the mode-line of an inactive window.")
(defface +modeline-highlight
'((t (:inherit mode-line-highlight)))
"Face used for highlighted modeline panels (like search counts).")
(defface +modeline-alternate-highlight
'((t (:inherit mode-line-highlight)))
"Alternative face used for highlighted modeline panels (like search counts).")
;;
;;; Helpers
(defvar +modeline--redisplayed-p nil)
(defadvice! modeline-recalculate-height-a (&optional _force &rest _ignored)
"Ensure that window resizing functions take modeline height into account."
:before '(fit-window-to-buffer resize-temp-buffer-window)
(unless +modeline--redisplayed-p
(setq-local +modeline--redisplayed-p t)
(redisplay t)))
;;; `active'
(defvar +modeline--active-window (selected-window))
(defun +modeline-active ()
"Return non-nil if the selected window has an active modeline."
(eq (selected-window) +modeline--active-window))
(add-hook! 'pre-redisplay-functions
(defun +modeline-set-selected-window-h (&rest _)
"Track the active modeline's window in `+modeline--active-window'."
(let ((win (selected-window)))
(unless (minibuffer-window-active-p win)
(setq +modeline--active-window (frame-selected-window))))))
(defun +modeline--make-xpm (color width height)
"Create an XPM bitmap via COLOR, WIDTH and HEIGHT. Inspired by `powerline''s `pl/+modeline--make-xpm'."
(propertize
" " 'display
(let ((data (make-list height (make-list width 1)))
(color (or color "None")))
(ignore-errors
(create-image
(concat
(format "/* XPM */\nstatic char * percent[] = {\n\"%i %i 2 1\",\n\". c %s\",\n\" c %s\","
(length (car data))
(length data)
color
color)
(apply #'concat
(cl-loop with idx = 0
with len = (length data)
for dl in data
do (cl-incf idx)
collect
(concat "\""
(cl-loop for d in dl
if (= d 0) collect (string-to-char " ")
else collect (string-to-char "."))
(if (eq idx len) "\"};" "\",\n")))))
'xpm t :ascent 'center)))))
(defun +modeline-format-icon (icon label &optional face help-echo voffset)
(propertize (concat (all-the-icons-material
icon
:face face
:height 1.1
:v-adjust (or voffset -0.225))
(propertize label 'face face))
'help-echo help-echo))
(defun set-modeline! (name &optional default)
"Set the modeline to NAME.
If DEFAULT is non-nil, apply to all future buffers. Modelines are defined with
`def-modeline!'."
(if-let (format (assq name +modeline-format-alist))
(cl-destructuring-bind (lhs . rhs) (cdr format)
(if default
(setq-default +modeline-format-left lhs
+modeline-format-right rhs)
(setq +modeline-format-left lhs
+modeline-format-right rhs)))
(error "Could not find %S modeline format" name)))
(defun set-modeline-hook! (hooks name)
"Set the modeline to NAME on HOOKS.
See `def-modeline!' on how modelines are defined."
(let ((fn (intern (format "+modeline-set-%s-format-h" name))))
(dolist (hook (doom-enlist hooks))
(add-hook hook fn))))
(defmacro def-modeline! (name lhs rhs)
"Define a modeline format by NAME.
LHS and RHS are the formats representing the left and right hand side of the
mode-line, respectively. See the variable `format-mode-line' for details on what
LHS and RHS will accept."
`(progn
(setf (alist-get ',name +modeline-format-alist)
(cons ,lhs ,rhs))
(defun ,(intern (format "+modeline-set-%s-format-h" name)) (&rest _)
"TODO"
(set-modeline! ',name))))
(defmacro def-modeline-var! (name body &optional docstring &rest plist)
"TODO"
(unless (stringp docstring)
(push docstring plist)
(setq docstring nil))
`(progn
(,(if (plist-get plist :local) 'defvar-local 'defvar)
,name ,body ,docstring)
(put ',name 'risky-local-variable t)))
;;
;;; Segments
(def-modeline-var! +modeline-format-left nil
"The left-hand side of the modeline."
:local t)
(def-modeline-var! +modeline-format-right nil
"The right-hand side of the modeline."
:local t)
;;; `+modeline-bar'
(progn
(def-modeline-var! +modeline-bar "")
(def-modeline-var! +modeline-inactive-bar "")
(add-hook! '(doom-init-ui-hook doom-load-theme-hook) :append
(defun +modeline-refresh-bars-h ()
(let ((width (or +modeline-bar-width 1))
(height (max +modeline-height 0)))
(setq +modeline-bar
(+modeline--make-xpm
(and +modeline-bar-width
(face-background '+modeline-bar nil 'inherit))
width height)
+modeline-inactive-bar
(+modeline--make-xpm
(and +modeline-bar-width
(face-background '+modeline-bar-inactive nil 'inherit))
width height)))))
(add-hook! 'doom-change-font-size-hook
(defun +modeline-adjust-height-h ()
(defvar +modeline--old-height +modeline-height)
(let ((default-height +modeline--old-height)
(scale (or (frame-parameter nil 'font-scale) 0)))
(setq +modeline-height
(if (> scale 0)
(+ default-height (* (or (frame-parameter nil 'font-scale) 1)
doom-font-increment))
default-height))
(when doom-init-time
(+modeline-refresh-bars-h))))))
;;; `+modeline-matches'
(progn
(use-package! anzu
:after-call isearch-mode
:config
;; anzu and evil-anzu expose current/total state that can be displayed in the
;; mode-line.
(defadvice! +modeline-fix-anzu-count-a (positions here)
"Calulate anzu counts via POSITIONS and HERE."
:override #'anzu--where-is-here
(cl-loop for (start . end) in positions
collect t into before
when (and (>= here start) (<= here end))
return (length before)
finally return 0))
(setq anzu-cons-mode-line-p nil) ; manage modeline segment ourselves
;; Ensure anzu state is cleared when searches & iedit are done
(add-hook 'isearch-mode-end-hook #'anzu--reset-status 'append)
(add-hook 'iedit-mode-end-hook #'anzu--reset-status)
(advice-add #'evil-force-normal-state :before #'anzu--reset-status)
;; Fix matches segment mirroring across all buffers
(mapc #'make-variable-buffer-local
'(anzu--total-matched anzu--current-position anzu--state
anzu--cached-count anzu--cached-positions anzu--last-command
anzu--last-isearch-string anzu--overflow-p)))
(use-package! evil-anzu
:when (featurep! :editor evil)
:after-call (evil-ex-start-search evil-ex-start-word-search evil-ex-search-activate-highlight))
(defun +modeline--anzu ()
"Show the match index and total number thereof.
Requires `anzu', also `evil-anzu' if using `evil-mode' for compatibility with
`evil-search'."
(when (and (bound-and-true-p anzu--state)
(not (bound-and-true-p iedit-mode)))
(propertize
(let ((here anzu--current-position)
(total anzu--total-matched))
(cond ((eq anzu--state 'replace-query)
(format " %d replace " anzu--cached-count))
((eq anzu--state 'replace)
(format " %d/%d " here total))
(anzu--overflow-p
(format " %s+ " total))
(t
(format " %s/%d " here total))))
'face (if (+modeline-active) '+modeline-highlight))))
(defun +modeline--evil-substitute ()
"Show number of matches for evil-ex substitutions and highlights in real time."
(when (and (bound-and-true-p evil-local-mode)
(or (assq 'evil-ex-substitute evil-ex-active-highlights-alist)
(assq 'evil-ex-global-match evil-ex-active-highlights-alist)
(assq 'evil-ex-buffer-match evil-ex-active-highlights-alist)))
(propertize
(let ((range (if evil-ex-range
(cons (car evil-ex-range) (cadr evil-ex-range))
(cons (line-beginning-position) (line-end-position))))
(pattern (car-safe (evil-delimited-arguments evil-ex-argument 2))))
(if pattern
(format " %s matches " (how-many pattern (car range) (cdr range)))
" - "))
'face (if (+modeline-active) '+modeline-highlight))))
(defun +modeline--multiple-cursors ()
"Show the number of multiple cursors."
(when (bound-and-true-p evil-mc-cursor-list)
(let ((count (length evil-mc-cursor-list)))
(when (> count 0)
(let ((face (cond ((not (+modeline-active)) 'mode-line-inactive)
(evil-mc-frozen '+modeline-highlight)
('+modeline-alternate-highlight))))
(concat (propertize " " 'face face)
(all-the-icons-faicon "i-cursor" :face face :v-adjust -0.0575)
(propertize " " 'face `(:inherit (variable-pitch ,face)))
(propertize (format "%d " count)
'face face)))))))
(defun +modeline--overlay< (a b)
"Sort overlay A and B."
(< (overlay-start a) (overlay-start b)))
(defun +modeline--iedit ()
"Show the number of iedit regions matches + what match you're on."
(when (and (bound-and-true-p iedit-mode)
(bound-and-true-p iedit-occurrences-overlays))
(propertize
(let ((this-oc (or (let ((inhibit-message t))
(iedit-find-current-occurrence-overlay))
(save-excursion
(iedit-prev-occurrence)
(iedit-find-current-occurrence-overlay))))
(length (length iedit-occurrences-overlays)))
(format " %s/%d "
(if this-oc
(- length
(length (memq this-oc (sort (append iedit-occurrences-overlays nil)
#'+modeline--overlay<)))
-1)
"-")
length))
'face (if (+modeline-active) '+modeline-highlight))))
(defun +modeline--macro-recording ()
"Display current Emacs or evil macro being recorded."
(when (and (+modeline-active)
(or defining-kbd-macro
executing-kbd-macro))
(let ((sep (propertize " " 'face '+modeline-highlight)))
(concat sep
(propertize (if (bound-and-true-p evil-this-macro)
(char-to-string evil-this-macro)
"Macro")
'face '+modeline-highlight)
sep
(all-the-icons-octicon "triangle-right"
:face '+modeline-highlight
:v-adjust -0.05)
sep))))
(def-modeline-var! +modeline-matches
'(:eval
(let ((meta (concat (+modeline--macro-recording)
(+modeline--anzu)
(+modeline--evil-substitute)
(+modeline--iedit)
(+modeline--multiple-cursors))))
(or (and (not (equal meta "")) meta)
" %I ")))))
;;; `+modeline-modes'
(def-modeline-var! +modeline-modes ; remove minor modes
'(""
(:propertize mode-name
face bold
mouse-face +modeline-highlight)
mode-line-process
"%n"
" "))
;;; `+modeline-buffer-identification'
(def-modeline-var! +modeline-buffer-identification ; slightly more informative buffer id
'((:eval
(propertize
(let ((buffer-file-name (buffer-file-name (buffer-base-buffer))))
(or (when buffer-file-name
(if-let (project (doom-project-root buffer-file-name))
(let ((filename (or buffer-file-truename (file-truename buffer-file-name))))
(file-relative-name filename (concat project "..")))))
"%b"))
'face (cond ((buffer-modified-p)
'(error bold mode-line-buffer-id))
((+modeline-active)
'mode-line-buffer-id))
'help-echo buffer-file-name))
(buffer-read-only (:propertize " RO" face warning))))
;;; `+modeline-position'
(def-modeline-var! +modeline-position '(" %l:%C %p "))
;;; `+modeline-checker'
(progn
(def-modeline-var! +modeline-checker nil
"Displays color-coded error status & icon for the current buffer."
:local t)
(add-hook! '(flycheck-status-changed-functions
flycheck-mode-hook)
(defun +modeline-checker-update (&optional status)
"Update flycheck text via STATUS."
(setq +modeline-checker
(pcase status
(`finished
(if flycheck-current-errors
(let-alist (flycheck-count-errors flycheck-current-errors)
(let ((error (or .error 0))
(warning (or .warning 0))
(info (or .info 0)))
(+modeline-format-icon "do_not_disturb_alt"
(number-to-string (+ error warning info))
(cond ((> error 0) 'error)
((> warning 0) 'warning)
('success))
(format "Errors: %d, Warnings: %d, Debug: %d"
error
warning
info))))
(+modeline-format-icon "check" "" 'success)))
(`running (+modeline-format-icon "access_time" "*" 'font-lock-comment-face "Running..."))
(`errored (+modeline-format-icon "sim_card_alert" "!" 'error "Errored!"))
(`interrupted (+modeline-format-icon "pause" "!" 'font-lock-comment-face "Interrupted"))
(`suspicious (+modeline-format-icon "priority_high" "!" 'error "Suspicious")))))))
;;; `+modeline-selection-info'
(progn
(defsubst +modeline--column (pos)
"Get the column of the position `POS'."
(save-excursion (goto-char pos)
(current-column)))
(def-modeline-var! +modeline-selection-info
'(:eval
(when (or mark-active
(and (bound-and-true-p evil-local-mode)
(eq evil-state 'visual)))
(cl-destructuring-bind (beg . end)
(if (boundp 'evil-local-mode)
(cons evil-visual-beginning evil-visual-end)
(cons (region-beginning) (region-end)))
(propertize
(let ((lines (count-lines beg (min end (point-max)))))
(concat " "
(cond ((or (bound-and-true-p rectangle-mark-mode)
(and (bound-and-true-p evil-visual-selection)
(eq 'block evil-visual-selection)))
(let ((cols (abs (- (+modeline--column end)
(+modeline--column beg)))))
(format "%dx%dB" lines cols)))
((and (bound-and-true-p evil-visual-selection)
(eq evil-visual-selection 'line))
(format "%dL" lines))
((> lines 1)
(format "%dC %dL" (- end beg) lines))
((format "%dC" (- end beg))))
(when (derived-mode-p 'text-mode)
(format " %dW" (count-words beg end)))
" "))
'face (if (+modeline-active) 'success)))))
"Information about the current selection, such as how many characters and
lines are selected, or the NxM dimensions of a block selection.")
(defun +modeline-add-selection-segment-h ()
(add-to-list '+modeline-format-left '+modeline-selection-info 'append))
(defun +modeline-remove-selection-segment-h ()
(delq! '+modeline-selection-info +modeline-format-left))
(if (featurep 'evil)
(progn
(add-hook 'evil-visual-state-entry-hook #'+modeline-add-selection-segment-h)
(add-hook 'evil-visual-state-exit-hook #'+modeline-remove-selection-segment-h))
(add-hook 'activate-mark-hook #'+modeline-add-selection-segment-h)
(add-hook 'deactivate-mark-hook #'+modeline-remove-selection-segment-h)))
;;; `+modeline-encoding'
(def-modeline-var! +modeline-encoding
'(:eval
(concat (pcase (coding-system-eol-type buffer-file-coding-system)
(0 " LF ")
(1 " RLF ")
(2 " CR "))
(let ((sys (coding-system-plist buffer-file-coding-system)))
(if (memq (plist-get sys :category)
'(coding-category-undecided coding-category-utf-8))
"UTF-8"
(upcase (symbol-name (plist-get sys :name)))))
" ")))
;;
;;; Default modeline
(def-modeline! :main
'(""
+modeline-matches
" "
+modeline-buffer-identification
+modeline-position)
'(""
mode-line-misc-info
+modeline-modes
(vc-mode (" "
,(all-the-icons-octicon "git-branch" :v-adjust 0.0)
vc-mode " "))
" "
+modeline-encoding
(+modeline-checker ("" +modeline-checker " "))))
(def-modeline! project
`(" "
,(all-the-icons-octicon
"file-directory"
:face 'bold
:v-adjust -0.05
:height 1.25)
(:propertize (" " (:eval (abbreviate-file-name default-directory)))
face bold))
'("" +modeline-modes))
(def-modeline! special
'("" +modeline-matches
" " +modeline-buffer-identification)
'("" +modeline-modes))
;; TODO (def-modeline! pdf ...)
;; TODO (def-modeline! helm ...)
;;
;;; Bootstrap
(size-indication-mode +1) ; filesize in modeline
(setq-default
mode-line-format
'(""
+modeline-bar
+modeline-format-left
(:eval
(propertize
" "
'display
`((space :align-to (- (+ right right-fringe right-margin)
,(string-width
(format-mode-line '("" +modeline-format-right))))))))
+modeline-format-right))
(with-current-buffer "*Messages*"
(setq mode-line-format (default-value 'mode-line-format)))
;; Other modes
(set-modeline! :main 'default)
(set-modeline-hook! '+doom-dashboard-mode-hook 'project)
(set-modeline-hook! 'pdf-tools-enabled-hook 'pdf)
(set-modeline-hook! '(special-mode-hook
image-mode-hook
circe-mode-hook)
'special)
(add-hook! 'magit-mode-hook
(defun +modeline-init-project-or-hide-h ()
(if (eq major-mode 'magit-status-mode)
(set-modeline! 'project)
(hide-mode-line-mode +1))))

View File

@@ -0,0 +1,120 @@
#+TITLE: ui/modeline
#+DATE: July 29, 2018
#+SINCE: v2.0.9
#+STARTUP: inlineimages
* Table of Contents :TOC_2:noexport:
- [[#description][Description]]
- [[#module-flags][Module Flags]]
- [[#plugins][Plugins]]
- [[#prerequisites][Prerequisites]]
- [[#usage][Usage]]
- [[#hiding-the-modeline][Hiding the modeline]]
- [[#switching-the-modeline-and-header-line][Switching the modeline and header line]]
- [[#configuration][Configuration]]
- [[#changing-the-default-modeline][Changing the default modeline]]
- [[#activating-a-format][Activating a format]]
- [[#defining-a-modeline-format][Defining a modeline format]]
- [[#defining-a-modeline-segment][Defining a modeline segment]]
- [[#extracting-dooms-modeline-into-your-config][Extracting Doom's modeline into your config]]
- [[#troubleshooting][Troubleshooting]]
- [[#where-are-my-minor-modes][Where are my minor modes?]]
- [[#icons-in-my-modeline-look-strange][Icons in my modeline look strange]]
- [[#appendix][Appendix]]
- [[#autodefs][Autodefs]]
- [[#variables][Variables]]
- [[#faces][Faces]]
* Description
This module provides an Atom-inspired, minimalistic modeline for Doom Emacs,
powered by [[https://github.com/seagle0128/doom-modeline][the doom-modeline package]] (where you can find screenshots).
** Module Flags
This module provides no flags.
** Plugins
+ [[https://github.com/seagle0128/doom-modeline][doom-modeline]]
+ [[https://github.com/syohex/emacs-anzu][anzu]]
+ [[https://github.com/syohex/emacs-evil-anzu][evil-anzu]]
* Prerequisites
This module has no prerequisites.
* Usage
** TODO Hiding the modeline
** TODO Switching the modeline and header line
* Configuration
** TODO Changing the default modeline
** TODO Activating a format
** TODO Defining a modeline format
** TODO Defining a modeline segment
** TODO Extracting Doom's modeline into your config
* Troubleshooting
** Where are my minor modes?
I rarely need to know what minor modes are active, so I removed them. ~M-x
doom/describe-active-minor-mode~ was written to substitute for it.
** TODO Icons in my modeline look strange
* Appendix
** Autodefs
+ ~def-modeline-format! NAME LEFT &optional RIGHT~
+ ~def-modeline-segment! NAME &rest REST~
+ ~set-modeline! NAME &optional DEFAULT~
** Variables
+ doom-modeline-height
+ doom-modeline-bar-width
+ doom-modeline-buffer-file-name-style
+ doom-modeline-icon
+ doom-modeline-major-mode-icon
+ doom-modeline-major-mode-color-icon
+ doom-modeline-buffer-state-icon
+ doom-modeline-buffer-modification-icon
+ doom-modeline-minor-modes
+ doom-modeline-enable-word-count
+ doom-modeline-buffer-encoding
+ doom-modeline-indent-info
+ doom-modeline-checker-simple-format
+ doom-modeline-vcs-max-length
+ doom-modeline-persp-name
+ doom-modeline-lsp
+ doom-modeline-github
+ doom-modeline-github-interval
+ doom-modeline-env-version
+ doom-modeline-mu4e
+ doom-modeline-irc
+ doom-modeline-irc-stylize
** Faces
+ doom-modeline-buffer-path
+ doom-modeline-buffer-file
+ doom-modeline-buffer-modified
+ doom-modeline-buffer-major-mode
+ doom-modeline-buffer-minor-mode
+ doom-modeline-project-parent-dir
+ doom-modeline-project-dir
+ doom-modeline-project-root-dir
+ doom-modeline-highlight
+ doom-modeline-panel
+ doom-modeline-debug
+ doom-modeline-info
+ doom-modeline-warning
+ doom-modeline-urgent
+ doom-modeline-unread-number
+ doom-modeline-bar
+ doom-modeline-inactive-bar
+ doom-modeline-evil-emacs-state
+ doom-modeline-evil-insert-state
+ doom-modeline-evil-motion-state
+ doom-modeline-evil-normal-state
+ doom-modeline-evil-operator-state
+ doom-modeline-evil-visual-state
+ doom-modeline-evil-replace-state
+ doom-modeline-persp-name
+ doom-modeline-persp-buffer-not-in-persp

View File

@@ -0,0 +1,48 @@
;;; ui/modeline/autoload/modeline.el -*- lexical-binding: t; -*-
;;;###autodef
(defalias 'def-modeline-format! #'doom-modeline-def-modeline)
;;;###autodef
(defalias 'def-modeline-segment! #'doom-modeline-def-segment)
;;;###autodef
(defalias 'set-modeline! #'doom-modeline-set-modeline)
(defvar +modeline--old-bar-height nil)
;;;###autoload
(defun +modeline-resize-for-font-h ()
"Adjust the modeline's height when the font size is changed by
`doom/increase-font-size' or `doom/decrease-font-size'.
Meant for `doom-change-font-size-hook'."
(unless +modeline--old-bar-height
(setq +modeline--old-bar-height doom-modeline-height))
(let ((default-height +modeline--old-bar-height)
(scale (or (frame-parameter nil 'font-scale) 0)))
(if (> scale 0)
(let* ((font-size (string-to-number
(aref (doom--font-name (frame-parameter nil 'font)
(selected-frame))
xlfd-regexp-pixelsize-subnum)))
(scale (frame-parameter nil 'font-scale)))
(setq doom-modeline-height (+ default-height (* scale doom-font-increment))))
(setq doom-modeline-height default-height))))
;;;###autoload
(defun +modeline-update-env-in-all-windows-h (&rest _)
"Update version strings in all buffers."
(dolist (window (window-list))
(with-selected-window window
(doom-modeline-update-env)
(force-mode-line-update))))
;;;###autoload
(defun +modeline-clear-env-in-all-windows-h (&rest _)
"Blank out version strings in all buffers."
(dolist (buffer (buffer-list))
(with-current-buffer buffer
(setq doom-modeline-env--version
(bound-and-true-p doom-modeline-load-string))))
(force-mode-line-update t))

View File

@@ -0,0 +1,79 @@
;;; ui/modeline/config.el -*- lexical-binding: t; -*-
(when (featurep! +light)
(load! "+light"))
(use-package! doom-modeline
:unless (featurep! +light)
:hook (after-init . doom-modeline-mode)
:init
(unless after-init-time
;; prevent flash of unstyled modeline at startup
(setq-default mode-line-format nil))
;; We display project info in the modeline ourselves
(setq projectile-dynamic-mode-line nil)
;; Set these early so they don't trigger variable watchers
(setq doom-modeline-bar-width 3
doom-modeline-github nil
doom-modeline-mu4e nil
doom-modeline-persp-name nil
doom-modeline-minor-modes nil
doom-modeline-major-mode-icon nil
doom-modeline-buffer-file-name-style 'relative-from-project)
;; Fix modeline icons in daemon-spawned graphical frames. We have our own
;; mechanism for disabling all-the-icons, so we don't need doom-modeline to do
;; it for us. However, this may cause unwanted padding in the modeline in
;; daemon-spawned terminal frames. If it bothers you, you may prefer
;; `doom-modeline-icon' set to `nil'.
(when (daemonp)
(setq doom-modeline-icon t))
:config
;; Fix an issue where these two variables aren't defined in TTY Emacs on MacOS
(defvar mouse-wheel-down-event nil)
(defvar mouse-wheel-up-event nil)
(size-indication-mode +1) ; filesize in modeline
(column-number-mode +1) ; cursor column in modeline
(add-hook 'doom-change-font-size-hook #'+modeline-resize-for-font-h)
(add-hook 'doom-load-theme-hook #'doom-modeline-refresh-bars)
(add-hook '+doom-dashboard-mode-hook #'doom-modeline-set-project-modeline)
(add-hook! 'magit-mode-hook
(defun +modeline-hide-in-non-status-buffer-h ()
"Show minimal modeline in magit-status buffer, no modeline elsewhere."
(if (eq major-mode 'magit-status-mode)
(doom-modeline-set-project-modeline)
(hide-mode-line-mode))))
;; Remove unused segments & extra padding
(doom-modeline-def-modeline 'main
'(bar window-number matches buffer-info remote-host buffer-position selection-info)
'(objed-state misc-info persp-name irc mu4e github debug input-method buffer-encoding lsp major-mode process vcs checker))
(doom-modeline-def-modeline 'special
'(bar window-number matches buffer-info-simple buffer-position selection-info)
'(objed-state misc-info persp-name debug input-method irc-buffers buffer-encoding lsp major-mode process checker))
(doom-modeline-def-modeline 'project
'(bar window-number buffer-default-directory)
'(misc-info mu4e github debug battery " " major-mode process))
;; Some functions modify the buffer, causing the modeline to show a false
;; modified state, so force them to behave.
(defadvice! +modeline--inhibit-modification-hooks-a (orig-fn &rest args)
:around #'ws-butler-after-save
(with-silent-modifications (apply orig-fn args)))
;;
;;; Extensions
(use-package! anzu
:after-call isearch-mode)
(use-package! evil-anzu
:when (featurep! :editor evil)
:after-call evil-ex-start-search evil-ex-start-word-search evil-ex-search-activate-highlight))

View File

@@ -0,0 +1,8 @@
;; -*- no-byte-compile: t; -*-
;;; ui/modeline/packages.el
(unless (featurep! +light)
(package! doom-modeline))
(package! anzu)
(when (featurep! :editor evil)
(package! evil-anzu))

View File

@@ -0,0 +1,37 @@
#+TITLE: ui/nav-flash
#+DATE: June 4, 2017
#+SINCE: v2.0
#+STARTUP: inlineimages
* Table of Contents :TOC:
- [[#description][Description]]
- [[#plugins][Plugins]]
- [[#prerequisites][Prerequisites]]
- [[#configuration][Configuration]]
* Description
This module flashes the line around the cursor after any significant motion, to
make it easy to follow after big operations.
#+begin_quote
Tremendously helpful on large, 1600p+ or 4K displays.
#+end_quote
** Plugins
+ [[https://github.com/rolandwalker/nav-flash][nav-flash]]
* Prerequisites
This module has no dependencies.
* Configuration
By default, ~nav-flash~ will be triggered whenever ~recenter~ is called or an
entry is added to the jump-list (maanaged by better-jumper).
~recenter~ is called after many hooks and commands, such as:
+ better-jumper-post-jump-hook
+ rtags-after-find-file-hook
+ org-follow-link-hook
+ imenu-after-jump-hook
+ counsel-grep-post-action-hook
+ dumb-jump-after-jump-hook

View File

@@ -0,0 +1,45 @@
;;; ui/nav-flash/autoload.el -*- lexical-binding: t; -*-
(defvar +nav-flash--last-point nil)
;;;###autoload
(defun +nav-flash-blink-cursor (&rest _)
"Blinks the current line in the current window, to make it clear where the
cursor has landed (typically after a large motion, like switching windows or
jumping to another part of the file)."
(unless (minibufferp)
(nav-flash-show)
;; only show in the current window
(overlay-put compilation-highlight-overlay 'window (selected-window))))
;;;###autoload
(defun +nav-flash-blink-cursor-maybe (&rest _)
"Like `+nav-flash-blink-cursor', but no-ops if in special-mode or term-mode,
or triggered from one of `+nav-flash-exclude-commands'."
(unless (or (memq this-command +nav-flash-exclude-commands)
(bound-and-true-p so-long-minor-mode)
(derived-mode-p 'so-long-mode 'special-mode 'term-mode)
(and (equal (point-marker) (car +nav-flash--last-point))
(equal (selected-window) (cdr +nav-flash--last-point))))
(+nav-flash-blink-cursor)
(setq +nav-flash--last-point (cons (point-marker) (selected-window)))))
;;;###autoload
(defun +nav-flash-delayed-blink-cursor-h (&rest _)
"Like `+nav-flash-blink-cursor', but links after a tiny pause, in case it
isn't clear at run-time if the point will be in the correct window/buffer (like
for `org-follow-link-hook')."
(run-at-time 0.1 nil #'+nav-flash-blink-cursor-h))
;;;###autoload
(defalias '+nav-flash-blink-cursor-h #'+nav-flash-blink-cursor)
;;;###autoload
(defalias '+nav-flash-blink-cursor-maybe-h #'+nav-flash-blink-cursor-maybe)
;;;###autoload
(defalias '+nav-flash-blink-cursor-a #'+nav-flash-blink-cursor-maybe)
;;;###autoload
(defun +nav-flash/blink-cursor (&rest _)
"Blink current line using `nav-flash'."
(interactive)
(+nav-flash-blink-cursor))

View File

@@ -0,0 +1,33 @@
;;; ui/nav-flash/config.el -*- lexical-binding: t; -*-
(defvar +nav-flash-exclude-commands
'(mouse-set-point mouse-drag-region evil-mouse-drag-region +org/dwim-at-point
org-find-file org-find-file-at-mouse)
"A list of commands that should not trigger nav-flash.")
(use-package! nav-flash
:defer t
:init
;; NOTE In :tools lookup `recenter' is hooked to a bunch of jumping
;; commands, which will trigger nav-flash.
(add-hook! '(imenu-after-jump-hook
better-jumper-post-jump-hook
counsel-grep-post-action-hook
dumb-jump-after-jump-hook)
#'+nav-flash-blink-cursor-maybe-h)
(add-hook 'doom-switch-window-hook #'+nav-flash-blink-cursor-maybe-h)
;; `org'
(add-hook 'org-follow-link-hook #'+nav-flash-delayed-blink-cursor-h)
;; `saveplace'
(advice-add #'save-place-find-file-hook :after #'+nav-flash-blink-cursor-a)
;; `evil'
(advice-add #'evil-window-top :after #'+nav-flash-blink-cursor-a)
(advice-add #'evil-window-middle :after #'+nav-flash-blink-cursor-a)
(advice-add #'evil-window-bottom :after #'+nav-flash-blink-cursor-a)
;; Bound to `ga' for evil users
(advice-add #'what-cursor-position :after #'+nav-flash-blink-cursor-a))

View File

@@ -0,0 +1,4 @@
;; -*- no-byte-compile: t; -*-
;;; ui/nav-flash/packages.el
(package! nav-flash)

View File

@@ -0,0 +1,9 @@
#+TITLE: :evil neotree
This module brings a side panel for browsing project files, inspired by vim's
NERDTree.
#+begin_quote
Sure, there's dired and projectile, but sometimes I'd like a bird's eye view of
a project.
#+end_quote

View File

@@ -0,0 +1,71 @@
;;; ui/neotree/autoload.el -*- lexical-binding: t; -*-
;; `neotree-show' and `neotree-find' don't respect the current project, and open
;; neotree in `default-directory'. `+neotree/open' and `neotree/find-this-file'
;; will ensure the neotree pane is always rooted in the project root.
;;;###autoload
(defun +neotree/open ()
"Open the neotree window in the current project."
(interactive)
(require 'neotree)
(if (neo-global--window-exists-p)
(neotree-hide)
(neotree-dir (or (doom-project-root)
default-directory))))
;;;###autoload
(defun +neotree/find-this-file ()
"Open the neotree window in the current project, and find the current file."
(interactive)
(let ((path buffer-file-name)
(project-root (or (doom-project-root)
default-directory)))
(require 'neotree)
(cond ((and (neo-global--window-exists-p)
(get-buffer-window neo-buffer-name t))
(neotree-find path project-root)
(neotree-refresh))
((not (and (neo-global--window-exists-p)
(equal (file-truename (neo-global--with-buffer neo-buffer--start-node))
(file-truename project-root))))
(neotree-dir project-root)
(neotree-find path project-root))
(t
(neotree-find path project-root)))))
;;;###autoload
(defun +neotree/collapse-or-up ()
"Collapse an expanded directory node or go to the parent node."
(interactive)
(when-let (node (neo-buffer--get-filename-current-line))
(if (file-directory-p node)
(if (neo-buffer--expanded-node-p node)
(+neotree/collapse)
(neotree-select-up-node))
(neotree-select-up-node))))
;;;###autoload
(defun +neotree/collapse ()
"Collapse a neotree node."
(interactive)
(when-let (node (neo-buffer--get-filename-current-line))
(when (file-directory-p node)
(neo-buffer--set-expand node nil)
(neo-buffer--refresh t))
(when neo-auto-indent-point
(neo-point-auto-indent))))
;;;###autoload
(defun +neotree/expand-or-open ()
"Expand or open a neotree node."
(interactive)
(when-let (node (neo-buffer--get-filename-current-line))
(cond ((file-directory-p node)
(neo-buffer--set-expand node t)
(neo-buffer--refresh t)
(when neo-auto-indent-point
(forward-line)
(neo-point-auto-indent)))
(t
(call-interactively #'neotree-enter)))))

View File

@@ -0,0 +1,81 @@
;;; ui/neotree/config.el -*- lexical-binding: t; -*-
(use-package! neotree
:commands (neotree-show
neotree-hide
neotree-toggle
neotree-dir
neotree-find
neo-global--with-buffer
neo-global--window-exists-p)
:config
(setq neo-create-file-auto-open nil
neo-auto-indent-point nil
neo-autorefresh nil
neo-mode-line-type 'none
neo-window-width 24
neo-show-updir-line nil
neo-theme 'icons
neo-banner-message nil
neo-confirm-create-file #'off-p
neo-confirm-create-directory #'off-p
neo-show-hidden-files nil
neo-keymap-style 'concise
neo-show-hidden-files t
neo-hidden-regexp-list
'(;; vcs folders
"^\\.\\(?:git\\|hg\\|svn\\)$"
;; compiled files
"\\.\\(?:pyc\\|o\\|elc\\|lock\\|css.map\\|class\\)$"
;; generated files, caches or local pkgs
"^\\(?:node_modules\\|vendor\\|.\\(project\\|cask\\|yardoc\\|sass-cache\\)\\)$"
;; org-mode folders
"^\\.\\(?:sync\\|export\\|attach\\)$"
;; temp files
"~$"
"^#.*#$"))
(set-popup-rule! "^ ?\\*NeoTree" :ignore t)
(after! winner
(add-to-list 'winner-boring-buffers neo-buffer-name))
;; The cursor always sits at bol. `+neotree--fix-cursor-h' and
;; `+neotree--indent-cursor-a' change that behavior so that the cursor is
;; always on the first non-blank character on the line, in the neo buffer.
(add-hook! 'neo-enter-hook
(defun +neotree-fix-cursor-h (&rest _)
(with-current-buffer neo-global--buffer
(+neotree--indent-cursor-a))))
(defadvice! +neotree--indent-cursor-a (&rest _)
:after '(neotree-next-line neotree-previous-line)
(beginning-of-line)
(skip-chars-forward " \t\r"))
(map! :map neotree-mode-map
:n "g" nil
:n "TAB" #'neotree-quick-look
:n "RET" #'neotree-enter
:n [tab] #'neotree-quick-look
:n [return] #'neotree-enter
:n "DEL" #'evil-window-prev
:n "c" #'neotree-create-node
:n "r" #'neotree-rename-node
:n "d" #'neotree-delete-node
:n "j" #'neotree-next-line
:n "k" #'neotree-previous-line
:n "n" #'neotree-next-line
:n "p" #'neotree-previous-line
:n "h" #'+neotree/collapse-or-up
:n "l" #'+neotree/expand-or-open
:n "J" #'neotree-select-next-sibling-node
:n "K" #'neotree-select-previous-sibling-node
:n "H" #'neotree-select-up-node
:n "L" #'neotree-select-down-node
:n "G" #'evil-goto-line
:n "gg" #'evil-goto-first-line
:n "v" #'neotree-enter-vertical-split
:n "s" #'neotree-enter-horizontal-split
:n "q" #'neotree-hide
:n "R" #'neotree-refresh))

View File

@@ -0,0 +1,4 @@
;; -*- no-byte-compile: t; -*-
;;; ui/neotree/packages.el
(package! neotree)

View File

@@ -0,0 +1,23 @@
#+TITLE: ui/ophints
#+DATE: June 4, 2017
#+SINCE: v2.0
#+STARTUP: inlineimages
* Table of Contents :TOC_3:noexport:
- [[#description][Description]]
- [[#module-flags][Module Flags]]
- [[#plugins][Plugins]]
* Description
This module provides op-hints (operation hinting), i.e. visual feedback for
certain operations. It highlights regions of text that the last operation (like
yank) acted on.
Uses ~evil-goggles~ for evil users and ~volatile-highlights~ otherwise.
** Module Flags
This module provides no flags.
** Plugins
+ [[https://github.com/edkolev/evil-goggles/][evil-goggles]]*
+ [[https://github.com/k-talo/volatile-highlights.el][volatile-highlights]]*

View File

@@ -0,0 +1,33 @@
;;; ui/ophints/config.el -*- lexical-binding: t; -*-
(use-package! evil-goggles
:when (featurep! :editor evil)
:after-call pre-command-hook
:init
(setq evil-goggles-duration 0.1
evil-goggles-pulse nil ; too slow
;; evil-goggles provides a good indicator of what has been affected.
;; delete/change is obvious, so I'd rather disable it for these.
evil-goggles-enable-delete nil
evil-goggles-enable-change nil)
:config
(pushnew! evil-goggles--commands
'(+evil:yank-unindented
:face evil-goggles-yank-face
:switch evil-goggles-enable-yank
:advice evil-goggles--generic-async-advice)
'(+eval:region
:face evil-goggles-yank-face
:switch evil-goggles-enable-yank
:advice evil-goggles--generic-async-advice))
(evil-goggles-mode +1))
(use-package! volatile-highlights
:unless (featurep! :editor evil)
:after-call pre-command-hook
:config
(volatile-highlights-mode)
(after! undo-tree
(vhl/define-extension 'undo-tree 'undo-tree-yank 'undo-tree-move)
(vhl/install-extension 'undo-tree)))

View File

@@ -0,0 +1,6 @@
;; -*- no-byte-compile: t; -*-
;;; ui/ophints/packages.el
(if (featurep! :editor evil)
(package! evil-goggles)
(package! volatile-highlights))

View File

@@ -0,0 +1,344 @@
;;; ui/popup/+hacks.el -*- lexical-binding: t; -*-
;; What follows are all the hacks needed to get various parts of Emacs and other
;; plugins to cooperate with the popup management system. Essentially, it comes
;; down to:
;;
;; 1. Making plugins that control their own window environment less greedy (e.g.
;; org agenda, which tries to reconfigure the entire frame by deleting all
;; other windows just to pop up one tiny window).
;; 2. Forcing plugins to use `display-buffer' and `pop-to-buffer' instead of
;; `switch-to-buffer' (which is unaffected by `display-buffer-alist', which
;; this module heavily relies on).
;; 3. Closing popups (temporarily) before functions that are highly destructive
;; to the illusion of popup control get run (with the use of the
;; `save-popups!' macro).
;;
;; Keep in mind, all this black magic may break in future updates, and will need
;; to be watched carefully for corner cases. Also, once this file is loaded,
;; many of its changes are irreversible without restarting Emacs! I don't like
;; it either, but I will address this over time.
;;
;; Hacks should be kept in alphabetical order, named after the feature they
;; modify, and should follow a ;;;## package-name header line (if not using
;; `after!' or `use-package!').
;;
;;; Core functions
;; Don't try to resize popup windows
(advice-add #'balance-windows :around #'+popup-save-a)
(defun +popup/quit-window ()
"The regular `quit-window' sometimes kills the popup buffer and switches to a
buffer that shouldn't be in a popup. We prevent that by remapping `quit-window'
to this commmand."
(interactive)
(let ((orig-buffer (current-buffer)))
(quit-window)
(when (and (eq orig-buffer (current-buffer))
(+popup-window-p))
(+popup/close))))
(global-set-key [remap quit-window] #'+popup/quit-window)
;;
;;; External functions
;;;###package buff-menu
(define-key Buffer-menu-mode-map (kbd "RET") #'Buffer-menu-other-window)
;;;###package company
(defadvice! +popup--dont-select-me-a (orig-fn &rest args)
:around #'company-show-doc-buffer
(let ((+popup--inhibit-select t))
(apply orig-fn args)))
;;;###package eshell
(progn
(setq eshell-destroy-buffer-when-process-dies t)
;; When eshell runs a visual command (see `eshell-visual-commands'), it spawns
;; a term buffer to run it in, but where it spawns it is the problem...
(defadvice! +popup--eshell-undedicate-popup (&rest _)
"Force spawned term buffer to share with the eshell popup (if necessary)."
:before #'eshell-exec-visual
(when (+popup-window-p)
(set-window-dedicated-p nil nil)
(add-transient-hook! #'eshell-query-kill-processes :after
(set-window-dedicated-p nil t)))))
;;;###package evil
(progn
;; Make evil-mode cooperate with popups
(defadvice! +popup--evil-command-window-a (hist cmd-key execute-fn)
"Monkey patch the evil command window to use `pop-to-buffer' instead of
`switch-to-buffer', allowing the popup manager to handle it."
:override #'evil-command-window
(when (eq major-mode 'evil-command-window-mode)
(user-error "Cannot recursively open command line window"))
(dolist (win (window-list))
(when (equal (buffer-name (window-buffer win))
"*Command Line*")
(kill-buffer (window-buffer win))
(delete-window win)))
(setq evil-command-window-current-buffer (current-buffer))
(ignore-errors (kill-buffer "*Command Line*"))
(with-current-buffer (pop-to-buffer "*Command Line*")
(setq-local evil-command-window-execute-fn execute-fn)
(setq-local evil-command-window-cmd-key cmd-key)
(evil-command-window-mode)
(evil-command-window-insert-commands hist)))
(defadvice! +popup--evil-command-window-execute-a ()
"Execute the command under the cursor in the appropriate buffer, rather than
the command buffer."
:override #'evil-command-window-execute
(interactive)
(let ((result (buffer-substring (line-beginning-position)
(line-end-position)))
(execute-fn evil-command-window-execute-fn)
(execute-window (get-buffer-window evil-command-window-current-buffer))
(popup (selected-window)))
(if execute-window
(select-window execute-window)
(user-error "Originating buffer is no longer active"))
;; (kill-buffer "*Command Line*")
(delete-window popup)
(funcall execute-fn result)
(setq evil-command-window-current-buffer nil)))
;; Don't mess with popups
(advice-add #'+evil--window-swap :around #'+popup-save-a)
(advice-add #'evil-window-move-very-bottom :around #'+popup-save-a)
(advice-add #'evil-window-move-very-top :around #'+popup-save-a)
(advice-add #'evil-window-move-far-left :around #'+popup-save-a)
(advice-add #'evil-window-move-far-right :around #'+popup-save-a))
;;;###package help-mode
(after! help-mode
(defun +popup--switch-from-popup (location)
(let (origin enable-local-variables)
(save-popups!
(switch-to-buffer (car location) nil t)
(if (not (cdr location))
(message "Unable to find location in file")
(goto-char (cdr location))
(recenter)
(setq origin (selected-window))))
(select-window origin)))
;; Help buffers use `pop-to-window' to decide where to open followed links,
;; which can be unpredictable. It should *only* replace the original buffer we
;; opened the popup from. To fix this these three button types need to be
;; redefined to set aside the popup before following a link.
(define-button-type 'help-function-def
:supertype 'help-xref
'help-function
(lambda (fun file)
(require 'find-func)
(when (eq file 'C-source)
(setq file (help-C-file-name (indirect-function fun) 'fun)))
(+popup--switch-from-popup (find-function-search-for-symbol fun nil file))))
(define-button-type 'help-variable-def
:supertype 'help-xref
'help-function
(lambda (var &optional file)
(when (eq file 'C-source)
(setq file (help-C-file-name var 'var)))
(+popup--switch-from-popup (find-variable-noselect var file))))
(define-button-type 'help-face-def
:supertype 'help-xref
'help-function
(lambda (fun file)
(require 'find-func)
(+popup--switch-from-popup (find-function-search-for-symbol fun 'defface file)))))
;;;###package helpful
(defadvice! +popup--helpful-open-in-origin-window-a (button)
"Open links in non-popup, originating window rather than helpful's window."
:override #'helpful--navigate
(let ((path (substring-no-properties (button-get button 'path)))
enable-local-variables
origin)
(save-popups!
(find-file path)
(when-let (pos (get-text-property button 'position
(marker-buffer button)))
(goto-char pos))
(setq origin (selected-window))
(recenter))
(select-window origin)))
;;;###package helm
;;;###package helm-ag
(when (featurep! :completion helm)
(setq helm-default-display-buffer-functions '(+popup-display-buffer-stacked-side-window-fn))
;; Fix #897: "cannot open side window" error when TAB-completing file links
(defadvice! +popup--helm-hide-org-links-popup-a (orig-fn &rest args)
:around #'org-insert-link
(cl-letf* ((old-org-completing-read (symbol-function 'org-completing-read))
((symbol-function 'org-completing-read)
(lambda (&rest args)
(when-let (win (get-buffer-window "*Org Links*"))
;; While helm is opened as a popup, it will mistaken the
;; *Org Links* popup for the "originated window", and will
;; target it for actions invoked by the user. However, since
;; *Org Links* is a popup too (they're dedicated side
;; windows), Emacs complains about being unable to split a
;; side window. The simple fix: get rid of *Org Links*!
(delete-window win)
;; But it must exist for org to clean up later.
(get-buffer-create "*Org Links*"))
(apply old-org-completing-read args))))
(apply #'funcall-interactively orig-fn args)))
;; Fix left-over popup window when closing persistent help for `helm-M-x'
(defadvice! +popup--helm-elisp--persistent-help-a (candidate _fun &optional _name)
:before #'helm-elisp--persistent-help
(let (win)
(when (and (helm-attr 'help-running-p)
(string= candidate (helm-attr 'help-current-symbol))
(setq win (get-buffer-window (get-buffer (help-buffer)))))
(delete-window win)))))
;;;###package Info
(defadvice! +popup--switch-to-info-window-a (&rest _)
:after #'info-lookup-symbol
(when-let (win (get-buffer-window "*info*"))
(when (+popup-window-p win)
(select-window win))))
;;;###package multi-term
(setq multi-term-buffer-name "doom terminal")
;;;###package neotree
(after! neotree
(advice-add #'neo-util--set-window-width :override #'ignore)
(advice-remove #'balance-windows #'ad-Advice-balance-windows))
;;;###package org
(after! org
;; Org has a scorched-earth window management policy I'm not fond of. i.e. it
;; kills all other windows just so it can monopolize the frame. No thanks. We
;; can do better ourselves.
(defadvice! +popup--suppress-delete-other-windows-a (orig-fn &rest args)
:around '(org-add-log-note
org-capture-place-template
org-export--dispatch-ui
org-agenda-get-restriction-and-command
org-fast-tag-selection
org-fast-todo-selection)
(if +popup-mode
(cl-letf (((symbol-function #'delete-other-windows)
(symbol-function #'ignore)))
(apply orig-fn args))
(apply orig-fn args)))
(defadvice! +popup--org-fix-popup-window-shrinking-a (orig-fn &rest args)
"Hides the mode-line in *Org tags* buffer so you can actually see its
content and displays it in a side window without deleting all other windows.
Ugh, such an ugly hack."
:around '(org-fast-tag-selection
org-fast-todo-selection)
(if +popup-mode
(cl-letf* ((old-fit-buffer-fn (symbol-function #'org-fit-window-to-buffer))
((symbol-function #'org-fit-window-to-buffer)
(lambda (&optional window max-height min-height shrink-only)
(when-let (buf (window-buffer window))
(delete-window window)
(select-window
(setq window (display-buffer-at-bottom buf nil)))
(with-current-buffer buf
(setq mode-line-format nil)))
(funcall old-fit-buffer-fn window max-height min-height shrink-only))))
(apply orig-fn args))
(apply orig-fn args)))
;; Ensure todo, agenda, and other minor popups are delegated to the popup system.
(defadvice! +popup--org-pop-to-buffer-a (orig-fn buf &optional norecord)
"Use `pop-to-buffer' instead of `switch-to-buffer' to open buffer.'"
:around #'org-switch-to-buffer-other-window
(if +popup-mode
(pop-to-buffer buf nil norecord)
(funcall orig-fn buf norecord))))
;;;###package persp-mode
(defadvice! +popup--persp-mode-restore-popups-a (&rest _)
"Restore popup windows when loading a perspective from file."
:after #'persp-load-state-from-file
(dolist (window (window-list))
(when (+popup-parameter 'popup window)
(+popup--init window nil))))
;;;###package pdf-tools
(after! pdf-tools
(setq tablist-context-window-display-action
'((+popup-display-buffer-stacked-side-window-fn)
(side . left)
(slot . 2)
(window-height . 0.3)
(inhibit-same-window . t))
pdf-annot-list-display-buffer-action
'((+popup-display-buffer-stacked-side-window-fn)
(side . left)
(slot . 3)
(inhibit-same-window . t))))
;;;###package profiler
(defadvice! +popup--profiler-report-find-entry-in-other-window-a (orig-fn function)
:around #'profiler-report-find-entry
(cl-letf (((symbol-function 'find-function)
(symbol-function 'find-function-other-window)))
(funcall orig-fn function)))
;;;###package wgrep
(progn
;; close the popup after you're done with a wgrep buffer
(advice-add #'wgrep-abort-changes :after #'+popup-close-a)
(advice-add #'wgrep-finish-edit :after #'+popup-close-a))
;;;###package which-key
(after! which-key
(when (eq which-key-popup-type 'side-window)
(setq which-key-popup-type 'custom
which-key-custom-popup-max-dimensions-function (lambda (_) (which-key--side-window-max-dimensions))
which-key-custom-hide-popup-function #'which-key--hide-buffer-side-window
which-key-custom-show-popup-function
(lambda (act-popup-dim)
(cl-letf (((symbol-function 'display-buffer-in-side-window)
(lambda (buffer alist)
(+popup-display-buffer-stacked-side-window-fn
buffer (append '((vslot . -9999)) alist)))))
(which-key--show-buffer-side-window act-popup-dim))))))
;;;###package windmove
;; Users should be able to hop into popups easily, but Elisp shouldn't.
(defadvice! +popup--ignore-window-parameters-a (orig-fn &rest args)
"Allow *interactive* window moving commands to traverse popups."
:around '(windmove-up windmove-down windmove-left windmove-right)
(cl-letf (((symbol-function #'windmove-find-other-window)
(lambda (dir &optional arg window)
(window-in-direction
(pcase dir (`up 'above) (`down 'below) (_ dir))
window (bound-and-true-p +popup-mode) arg windmove-wrap-around t))))
(apply orig-fn args)))

View File

@@ -0,0 +1,140 @@
#+TITLE: ui/popup
#+DATE: January 6, 2018
#+SINCE: v2.0.9
#+STARTUP: inlineimages
* Table of Contents :TOC:
- [[#description][Description]]
- [[#module-flags][Module Flags]]
- [[#prerequisites][Prerequisites]]
- [[#configuration][Configuration]]
- [[#set-popup-rule-and-set-popup-rules][~set-popup-rule!~ and ~set-popup-rules!~]]
- [[#disabling-hidden-mode-line-in-popups][Disabling hidden mode-line in popups]]
- [[#appendix][Appendix]]
- [[#commands][Commands]]
- [[#library][Library]]
- [[#hacks][Hacks]]
* Description
This module provides a customizable popup window management system.
Not all windows are created equally. Some are less important. Some I want gone
once they have served their purpose, like code output or a help buffer. Others I
want to stick around, like a scratch buffer or org-capture popup.
More than that, popups ought to be the second class citizens of my editor;
spawned off to the side, discarded with the push of a button (e.g. =ESC= or
=C-g=), and easily restored if I want to see them again. Of course, this system
should clean up after itself and kill off buffers I mark as transient.
** Module Flags
+ =+all= Enables fallback rules to ensure all temporary/special buffers (whose
name begins with a space or asterix) are treated as popups.
+ =+defaults= Enables reasonable default popup rules for a variety of buffers.
* Prerequisites
This module has no external prerequisites.
* Configuration
** ~set-popup-rule!~ and ~set-popup-rules!~
This module has two functions for defining your own rules for popups:
#+BEGIN_SRC emacs-lisp
(set-popup-rule! PREDICATE &key IGNORE ACTIONS SIDE SIZE WIDTH HEIGHT SLOT VSLOT TTL QUIT SELECT MODELINE AUTOSAVE PARAMETERS)
(set-popup-rules! &rest RULESETS)
#+END_SRC
~PREDICATE~ is a predicate function or regexp string to match against the
buffer's name. You'll find comprehensive documentation on the other keywords in
~set-popup-rule!~'s docstring (=SPC h f set-popup-rule!=).
#+begin_quote
Popup rules end up in ~display-buffer-alist~, which instructs ~display-buffer~
calls on how to set up windows for buffers that meet certain conditions.
However, some plugins can avoid it entirely if they use ~set-buffer~ or
~switch-to-buffer~, which don't obey ~display-buffer-alist~.
#+end_quote
Multiple popup rules can be defined with ~set-popup-rules!~:
#+BEGIN_SRC emacs-lisp
(set-popup-rules!
'(("^ \\*" :slot -1) ; fallback rule for special buffers
("^\\*" :select t)
("^\\*Completions" :slot -1 :ttl 0)
("^\\*\\(?:scratch\\|Messages\\)" :ttl t)
("^\\*Help" :slot -1 :size 0.2 :select t)
("^\\*doom:"
:size 0.35 :select t :modeline t :quit t :ttl t)))
#+END_SRC
Omitted parameters in a ~set-popup-rules!~ will use the defaults set in
~+popup-defaults~.
** Disabling hidden mode-line in popups
By default, the mode-line is hidden in popups. To disable this, you can either:
1. Change the default ~:modeline~ property in ~+popup-defaults~:
#+BEGIN_SRC emacs-lisp
;; add to $DOOMDIR/config.el
(plist-put +popup-defaults :modeline t)
#+END_SRC
A value of ~t~ will instruct popups to use the default mode-line. Any
popup rule with a ~:modeline~ property can still override this.
2. Completely disable management of the mode-line in popups:
#+BEGIN_SRC emacs-lisp
;; add to ~/.doom.d/config.el
(remove-hook '+popup-buffer-mode-hook #'+popup-set-modeline-on-enable-h)
#+END_SRC
* Appendix
** Commands
+ ~+popup/other~ (aliased to ~other-popup~, bound to ~C-x p~)
+ ~+popup/toggle~
+ ~+popup/close~
+ ~+popup/close-all~
+ ~+popup/toggle~
+ ~+popup/restore~
+ ~+popup/raise~
** Library
+ Functions
+ ~+popup-window-p WINDOW~
+ ~+popup-buffer-p BUFFER~
+ ~+popup-buffer BUFFER &optional ALIST~
+ ~+popup-parameter PARAMETER &optional WINDOW~
+ ~+popup-parameter-fn PARAMETER &optional WINDOW~
+ ~+popup-windows~
+ Macros
+ ~without-popups!~
+ ~save-popups!~
+ Hooks
+ ~+popup-adjust-fringes-h~
+ ~+popup|set-modeline~
+ ~+popup-close-on-escape-h~
+ ~+popup-cleanup-rules-h~
+ Minor modes
+ ~+popup-mode~
+ ~+popup-buffer-mode~
** Hacks
+ =help-mode= has been advised to follow file links in the buffer you were in
before entering the popup, rather than in a new window.
+ =wgrep= buffers are advised to close themselves when aborting or committing
changes.
+ =persp-mode= is advised to restore popup windows when loading a session from
file.
+ Interactive calls to ~windmove-*~ commands (used by ~evil-window-*~ commands)
will ignore the ~no-other-window~ window parameter, allowing you to switch to
popup windows as if they're ordinary windows.
+ ~balance-windows~ has been advised to close popups while it does its business,
then restore them afterwards.
+ =neotree= advises ~balance-windows~, which causes major slow-downs when paired
with our ~balance-window~ advice, so we removes neotree's advice.
+ =org-mode= is an ongoing (and huge) effort. It has a scorched-earth window
management system I'm not fond of. ie. it kills all windows and monopolizes
the frame. On top of that, it /really/ likes to use ~switch-to-buffer~ for
most of its buffer management, which completely bypasses
~display-buffer-alist~. Some work has gone into reversing this.

View File

@@ -0,0 +1,617 @@
;;; ui/popup/autoload/popup.el -*- lexical-binding: t; -*-
(defvar +popup--internal nil)
(defun +popup--remember (windows)
"Remember WINDOWS (a list of windows) for later restoration."
(cl-assert (cl-every #'windowp windows) t)
(setq +popup--last
(cl-loop for w in windows
collect (cons (window-buffer w)
(window-state-get w)))))
(defun +popup--kill-buffer (buffer ttl)
"Tries to kill BUFFER, as was requested by a transient timer. If it fails, eg.
the buffer is visible, then set another timer and try again later."
(when (buffer-live-p buffer)
(let ((inhibit-quit t)
(kill-buffer-hook (remq '+popup-kill-buffer-hook-h kill-buffer-hook)))
(cond ((get-buffer-window buffer t)
(with-current-buffer buffer
(setq +popup--timer
(run-at-time ttl nil #'+popup--kill-buffer buffer ttl))))
((eq ttl 0)
(kill-buffer buffer))
((with-demoted-errors "Error killing transient buffer: %s"
(with-current-buffer buffer
(let (confirm-kill-processes)
(when-let (process (get-buffer-process buffer))
(kill-process process))
(let (kill-buffer-query-functions)
;; HACK The debugger backtrace buffer, when killed, called
;; `top-level'. This causes jumpiness when the popup
;; manager tries to clean it up.
(cl-letf (((symbol-function #'top-level) #'ignore))
(kill-buffer buffer)))))))))))
(defun +popup--delete-window (window)
"Do housekeeping before destroying a popup window.
+ Disables `+popup-buffer-mode' so that any hooks attached to it get a chance to
run and do cleanup of its own.
+ Either kills the buffer or sets a transient timer, if the window has a
`transient' window parameter (see `+popup-window-parameters').
+ And finally deletes the window!"
(let ((buffer (window-buffer window))
(inhibit-quit t))
(and (buffer-file-name buffer)
(buffer-modified-p buffer)
(let ((autosave (+popup-parameter 'autosave window)))
(cond ((eq autosave 't))
((null autosave)
(y-or-n-p "Popup buffer is modified. Save it?"))
((functionp autosave)
(funcall autosave buffer))))
(with-current-buffer buffer (save-buffer)))
(let ((ignore-window-parameters t))
(if-let (wconf (window-parameter window 'saved-wconf))
(set-window-configuration wconf)
(delete-window window)))
(unless (window-live-p window)
(with-current-buffer buffer
(set-buffer-modified-p nil)
(+popup-buffer-mode -1)
(unless +popup--inhibit-transient
(let ((ttl (+popup-parameter 'ttl window)))
(when (eq ttl 't)
(setq ttl (plist-get +popup-defaults :ttl)))
(cond ((null ttl))
((functionp ttl)
(funcall ttl buffer))
((not (integerp ttl))
(signal 'wrong-type-argument (list 'integerp ttl)))
((= ttl 0)
(+popup--kill-buffer buffer 0))
((add-hook 'kill-buffer-hook #'+popup-kill-buffer-hook-h nil t)
(setq +popup--timer
(run-at-time ttl nil #'+popup--kill-buffer
buffer ttl))))))))))
(defun +popup--delete-other-windows (window)
"Fixes `delete-other-windows' when used from a popup window."
(when-let (window (ignore-errors (+popup/raise window)))
(let ((ignore-window-parameters t))
(delete-other-windows window)))
nil)
(defun +popup--normalize-alist (alist)
"Merge `+popup-default-alist' and `+popup-default-parameters' with ALIST."
(when alist
(let ((alist ; handle defaults
(cl-remove-duplicates
(append alist +popup-default-alist)
:key #'car-safe :from-end t))
(parameters
(cl-remove-duplicates
(append (cdr (assq 'window-parameters alist))
+popup-default-parameters)
:key #'car-safe :from-end t)))
;; handle `size'
(when-let* ((size (cdr (assq 'size alist)))
(side (or (cdr (assq 'side alist)) 'bottom))
(param (if (memq side '(left right))
'window-width
'window-height)))
(setq list (assq-delete-all 'size alist))
(setf (alist-get param alist) size))
(setf (alist-get 'window-parameters alist)
parameters)
;; Fixes #1305: addresses an edge case where a popup with a :size, :width
;; or :height greater than the current frame's dimensions causes
;; hanging/freezing (a bug in Emacs' `display-buffer' API perhaps?)
(let ((width (cdr (assq 'window-width alist)))
(height (cdr (assq 'window-height alist))))
(setf (alist-get 'window-width alist)
(if (numberp width)
(min width (frame-width))
width))
(setf (alist-get 'window-height alist)
(if (numberp height)
(min height (frame-height))
height))
alist))))
(defun +popup--split-window (window size side)
"Ensure a non-dedicated/popup window is selected when splitting a window."
(unless +popup--internal
(cl-loop for win
in (cons (or window (selected-window))
(window-list nil 0 window))
unless (+popup-window-p win)
return (setq window win)))
(let ((ignore-window-parameters t))
(split-window window size side)))
(defun +popup--maybe-select-window (window origin)
"Select a window based on `+popup--inhibit-select' and this window's `select' parameter."
(unless +popup--inhibit-select
(let ((select (+popup-parameter 'select window)))
(if (functionp select)
(funcall select window origin)
(select-window (if select window origin))))))
;;;###autoload
(defun +popup--init (window &optional alist)
"Initializes a popup window. Run any time a popup is opened. It sets the
default window parameters for popup windows, clears leftover transient timers
and enables `+popup-buffer-mode'."
(with-selected-window window
(setq alist (delq (assq 'actions alist) alist))
(set-window-parameter window 'popup t)
(set-window-parameter window 'split-window #'+popup--split-window)
(set-window-parameter window 'delete-window #'+popup--delete-window)
(set-window-parameter window 'delete-other-windows #'+popup--delete-other-windows)
(set-window-dedicated-p window 'popup)
(window-preserve-size
window (memq (window-parameter window 'window-side)
'(left right))
t)
(+popup-buffer-mode +1)
(run-hooks '+popup-create-window-hook)))
;;
;; Public library
;;;###autoload
(defun +popup-buffer-p (&optional buffer)
"Return non-nil if BUFFER is a popup buffer. Defaults to the current buffer."
(when +popup-mode
(let ((buffer (or buffer (current-buffer))))
(and (bufferp buffer)
(buffer-live-p buffer)
(buffer-local-value '+popup-buffer-mode buffer)
buffer))))
;;;###autoload
(defun +popup-window-p (&optional window)
"Return non-nil if WINDOW is a popup window. Defaults to the current window."
(when +popup-mode
(let ((window (or window (selected-window))))
(and (windowp window)
(window-live-p window)
(or (window-parameter window 'popup)
(window-parameter window 'no-other-window))
window))))
;;;###autoload
(defun +popup-buffer (buffer &optional alist)
"Open BUFFER in a popup window. ALIST describes its features."
(let* ((origin (selected-window))
(window-min-height 3)
(alist (+popup--normalize-alist alist))
(actions (or (cdr (assq 'actions alist))
+popup-default-display-buffer-actions)))
(or (let* ((alist (remove (assq 'window-width alist) alist))
(alist (remove (assq 'window-height alist) alist))
(window (display-buffer-reuse-window buffer alist)))
(when window
(+popup--maybe-select-window window origin)
window))
(when-let (popup (cl-loop for func in actions
if (funcall func buffer alist)
return it))
(+popup--init popup alist)
(+popup--maybe-select-window popup origin)
popup))))
;;;###autoload
(defun +popup-parameter (parameter &optional window)
"Fetch the window PARAMETER (symbol) of WINDOW"
(window-parameter (or window (selected-window)) parameter))
;;;###autoload
(defun +popup-parameter-fn (parameter &optional window &rest args)
"Fetch the window PARAMETER (symbol) of WINDOW. If it is a function, run it
with ARGS to get its return value."
(let ((val (+popup-parameter parameter window)))
(if (functionp val)
(apply val args)
val)))
;;;###autoload
(defun +popup-windows ()
"Returns a list of all popup windows."
(cl-remove-if-not #'+popup-window-p (window-list)))
;;;###autoload
(defun +popup-shrink-to-fit (&optional window)
"Shrinks WINDOW to fit the buffer contents, if the buffer isn't empty.
Uses `shrink-window-if-larger-than-buffer'."
(unless window
(setq window (selected-window)))
(unless (= (- (point-max) (point-min)) 0)
(shrink-window-if-larger-than-buffer window)))
;;;###autoload
(defun +popup-alist-from-window-state (state)
"Convert window STATE (from `window-state-get') to a `display-buffer' alist."
(let* ((params (alist-get 'parameters state)))
`((side . ,(alist-get 'window-side params))
(window-width . ,(alist-get 'total-width state))
(window-height . ,(alist-get 'total-height state))
(window-parameters ,@params))))
;;
;; Hooks
;;;###autoload
(defun +popup-adjust-fringes-h ()
"Hides the fringe in popup windows, restoring them if `+popup-buffer-mode' is
disabled."
(let ((f (if (bound-and-true-p +popup-buffer-mode) 0)))
(set-window-fringes nil f f fringes-outside-margins)))
;;;###autoload
(defun +popup-adjust-margins-h ()
"Creates padding for the popup window determined by `+popup-margin-width',
restoring it if `+popup-buffer-mode' is disabled."
(when +popup-margin-width
(unless (memq (window-parameter nil 'window-side) '(left right))
(let ((m (if (bound-and-true-p +popup-buffer-mode) +popup-margin-width)))
(set-window-margins nil m m)))))
(defvar hide-mode-line-format)
;;;###autoload
(defun +popup-set-modeline-on-enable-h ()
"Don't show modeline in popup windows without a `modeline' window-parameter.
Possible values for this parameter are:
t show the mode-line as normal
nil hide the modeline entirely (the default)
a function `mode-line-format' is set to its return value
Any non-nil value besides the above will be used as the raw value for
`mode-line-format'."
(when (bound-and-true-p +popup-buffer-mode)
(let ((modeline (+popup-parameter 'modeline)))
(cond ((eq modeline 't))
((null modeline)
;; TODO use `mode-line-format' window parameter instead (emacs 26+)
(hide-mode-line-mode +1))
((let ((hide-mode-line-format
(if (functionp modeline)
(funcall modeline)
modeline)))
(hide-mode-line-mode +1)))))))
(put '+popup-set-modeline-on-enable-h 'permanent-local-hook t)
;;;###autoload
(defun +popup-unset-modeline-on-disable-h ()
"Restore the modeline when `+popup-buffer-mode' is deactivated."
(when (and (not (bound-and-true-p +popup-buffer-mode))
(bound-and-true-p hide-mode-line-mode))
(hide-mode-line-mode -1)))
;;;###autoload
(defun +popup-close-on-escape-h ()
"If called inside a popup, try to close that popup window (see
`+popup/close'). If called outside, try to close all popup windows (see
`+popup/close-all')."
(if (+popup-window-p)
(+popup/close)
(+popup/close-all)))
;;;###autoload
(defun +popup-cleanup-rules-h ()
"Cleans up any duplicate popup rules."
(interactive)
(setq +popup--display-buffer-alist
(cl-delete-duplicates +popup--display-buffer-alist
:key #'car :test #'equal :from-end t))
(when +popup-mode
(setq display-buffer-alist +popup--display-buffer-alist)))
;;;###autoload
(defun +popup-kill-buffer-hook-h ()
"TODO"
(when-let (window (get-buffer-window))
(when (+popup-window-p window)
(let ((+popup--inhibit-transient t))
(+popup--delete-window window)))))
;;
;; Commands
;;;###autoload
(defalias 'other-popup #'+popup/other)
;;;###autoload
(defun +popup/buffer ()
"Open this buffer in a popup window."
(interactive)
(let ((+popup-default-display-buffer-actions
'(+popup-display-buffer-stacked-side-window-fn))
(display-buffer-alist +popup--display-buffer-alist)
(buffer (current-buffer)))
(push (+popup-make-rule "." +popup-defaults) display-buffer-alist)
(bury-buffer)
(pop-to-buffer buffer)))
;;;###autoload
(defun +popup/other ()
"Cycle through popup windows, like `other-window'. Ignores regular windows."
(interactive)
(if-let (popups (+popup-windows))
(select-window (if (+popup-window-p)
(let ((window (selected-window)))
(or (car-safe (cdr (memq window popups)))
(car (delq window popups))
(car popups)))
(car popups)))
(user-error "No popups are open")))
;;;###autoload
(defun +popup/close (&optional window force-p)
"Close WINDOW, if it's a popup window.
This will do nothing if the popup's `quit' window parameter is either nil or
'other. This window parameter is ignored if FORCE-P is non-nil."
(interactive
(list (selected-window)
current-prefix-arg))
(let ((window (or window (selected-window))))
(when (and (+popup-window-p window)
(or force-p
(memq (+popup-parameter-fn 'quit window window)
'(t current))))
(when +popup--remember-last
(+popup--remember (list window)))
(delete-window window)
t)))
;;;###autoload
(defun +popup/close-all (&optional force-p)
"Close all open popup windows.
This will ignore popups with an `quit' parameter that is either nil or 'current.
This window parameter is ignored if FORCE-P is non-nil."
(interactive "P")
(let (targets +popup--remember-last)
(dolist (window (+popup-windows))
(when (or force-p
(memq (+popup-parameter-fn 'quit window window)
'(t other)))
(push window targets)))
(when targets
(+popup--remember targets)
(mapc #'delete-window targets)
t)))
;;;###autoload
(defun +popup/toggle ()
"If popups are open, close them. If they aren't, restore the last one or open
the message buffer in a popup window."
(interactive)
(let ((+popup--inhibit-transient t))
(cond ((+popup-windows) (+popup/close-all t))
((ignore-errors (+popup/restore)))
((display-buffer (get-buffer "*Messages*"))))))
;;;###autoload
(defun +popup/restore ()
"Restore the last popups that were closed, if any."
(interactive)
(unless +popup--last
(error "No popups to restore"))
(cl-loop for (buffer . state) in +popup--last
if (buffer-live-p buffer)
do (+popup-buffer buffer (+popup-alist-from-window-state state)))
(setq +popup--last nil)
t)
;;;###autoload
(defun +popup/raise (window &optional arg)
"Raise the current popup window into a regular window.
If prefix ARG, raise the current popup into a new window."
(interactive
(list (selected-window) current-prefix-arg))
(cl-check-type window window)
(unless (+popup-window-p window)
(user-error "Cannot raise a non-popup window"))
(let ((buffer (current-buffer))
(+popup--inhibit-transient t)
+popup--remember-last)
(+popup/close window 'force)
(if arg
(pop-to-buffer buffer)
(switch-to-buffer buffer))))
;;;###autoload
(defun +popup/diagnose ()
"Reveal what popup rule will be used for the current buffer."
(interactive)
(or (cl-loop with bname = (buffer-name)
for (pred . action) in display-buffer-alist
if (and (functionp pred) (funcall pred bname action))
return (cons pred action)
else if (and (stringp pred) (string-match-p pred bname))
return (cons pred action))
(message "No popup rule for this buffer")))
;;
;;; Advice
;;;###autoload
(defun +popup-close-a (&rest _)
"TODO"
(+popup/close nil t))
;;;###autoload
(defun +popup-save-a (orig-fn &rest args)
"Sets aside all popups before executing the original function, usually to
prevent the popup(s) from messing up the UI (or vice versa)."
(save-popups! (apply orig-fn args)))
;;;###autoload
(defun +popup-display-buffer-fullframe-fn (buffer alist)
"Displays the buffer fullscreen."
(let ((wconf (current-window-configuration)))
(when-let (window (or (display-buffer-reuse-window buffer alist)
(display-buffer-same-window buffer alist)
(display-buffer-pop-up-window buffer alist)
(display-buffer-use-some-window buffer alist)))
(set-window-parameter window 'saved-wconf wconf)
(add-to-list 'window-persistent-parameters '(saved-wconf . t))
(delete-other-windows window)
window)))
;;;###autoload
(defun +popup-display-buffer-stacked-side-window-fn (buffer alist)
"A `display-buffer' action that serves as an alternative to
`display-buffer-in-side-window', but allows for stacking popups with the `vslot'
alist entry.
Accepts the same arguments as `display-buffer-in-side-window'. You must set
`window--sides-inhibit-check' to non-nil for this work properly."
(let* ((side (or (cdr (assq 'side alist)) 'bottom))
(slot (or (cdr (assq 'slot alist)) 0))
(vslot (or (cdr (assq 'vslot alist)) 0))
(left-or-right (memq side '(left right)))
(display-buffer-mark-dedicated (or display-buffer-mark-dedicated 'popup)))
(cond ((not (memq side '(top bottom left right)))
(error "Invalid side %s specified" side))
((not (numberp slot))
(error "Invalid slot %s specified" slot))
((not (numberp vslot))
(error "Invalid vslot %s specified" vslot)))
(let* ((major (get-window-with-predicate
(lambda (window)
(and (eq (window-parameter window 'window-side) side)
(eq (window-parameter window 'window-vslot) vslot)))
nil))
(reversed (window--sides-reverse-on-frame-p (selected-frame)))
(windows
(cond ((window-live-p major)
(list major))
((window-valid-p major)
(let* ((first (window-child major))
(next (window-next-sibling first))
(windows (list next first)))
(setq reversed (> (window-parameter first 'window-slot)
(window-parameter next 'window-slot)))
(while (setq next (window-next-sibling next))
(setq windows (cons next windows)))
(if reversed windows (nreverse windows))))))
(slots (if major (max 1 (window-child-count major))))
(max-slots
(nth (plist-get '(left 0 top 1 right 2 bottom 3) side)
window-sides-slots))
(window--sides-inhibit-check t)
window this-window this-slot prev-window next-window
best-window best-slot abs-slot)
(cond ((and (numberp max-slots) (<= max-slots 0))
nil)
((not windows)
(cl-letf (((symbol-function 'window--make-major-side-window-next-to)
(lambda (_side) (frame-root-window (selected-frame)))))
(when-let (window (window--make-major-side-window buffer side slot alist))
(set-window-parameter window 'window-vslot vslot)
(add-to-list 'window-persistent-parameters '(window-vslot . writable))
window)))
(t
;; Scan windows on SIDE.
(catch 'found
(dolist (window windows)
(setq this-slot (window-parameter window 'window-slot))
(cond ((not (numberp this-slot)))
((= this-slot slot) ; A window with a matching slot found
(setq this-window window)
(throw 'found t))
(t
;; Check if this window has a better slot value wrt the
;; slot of the window we want.
(setq abs-slot
(if (or (and (> this-slot 0) (> slot 0))
(and (< this-slot 0) (< slot 0)))
(abs (- slot this-slot))
(+ (abs slot) (abs this-slot))))
(unless (and best-slot (<= best-slot abs-slot))
(setq best-window window)
(setq best-slot abs-slot))
(if reversed
(cond
((<= this-slot slot)
(setq next-window window))
((not prev-window)
(setq prev-window window)))
(cond
((<= this-slot slot)
(setq prev-window window))
((not next-window)
(setq next-window window))))))))
;; `this-window' is the first window with the same SLOT.
;; `prev-window' is the window with the largest slot < SLOT. A new
;; window will be created after it.
;; `next-window' is the window with the smallest slot > SLOT. A new
;; window will be created before it.
;; `best-window' is the window with the smallest absolute
;; difference of its slot and SLOT.
(or (and this-window
;; Reuse `this-window'.
(with-current-buffer buffer
(setq window--sides-shown t))
(window--display-buffer
buffer this-window 'reuse alist))
(and (or (not max-slots) (< slots max-slots))
(or (and next-window
;; Make new window before `next-window'.
(let ((next-side (if left-or-right 'above 'left))
(+popup--internal t)
(window-combination-resize 'side))
(setq window
(ignore-errors (split-window next-window nil next-side)))))
(and prev-window
;; Make new window after `prev-window'.
(let ((prev-side (if left-or-right 'below 'right))
(+popup--internal t)
(window-combination-resize 'side))
(setq window
(ignore-errors (split-window prev-window nil prev-side))))))
(set-window-parameter window 'window-slot slot)
(with-current-buffer buffer
(setq window--sides-shown t))
(window--display-buffer
buffer window 'window alist))
(and best-window
;; Reuse `best-window'.
(progn
;; Give best-window the new slot value.
(set-window-parameter best-window 'window-slot slot)
(with-current-buffer buffer
(setq window--sides-shown t))
(window--display-buffer
buffer best-window 'reuse alist)))))))))
;;
;; Emacs backwards compatibility
(unless EMACS27+
(defadvice! +popup--set-window-dedicated-a (window)
"Ensure `window--display-buffer' respects `display-buffer-mark-dedicated'.
This was not so until recent Emacs 27 builds, where it causes breaking errors.
This advice ensures backwards compatibility for Emacs <= 26 users."
:filter-return #'window--display-buffer
(when (and (windowp window) display-buffer-mark-dedicated)
(set-window-dedicated-p window display-buffer-mark-dedicated))
window))

View File

@@ -0,0 +1,190 @@
;;; ui/popup/autoload/settings.el -*- lexical-binding: t; -*-
;;;###autoload
(defvar +popup--display-buffer-alist nil)
;;;###autoload
(defvar +popup-defaults
(list :side 'bottom
:height 0.16
:width 40
:quit t
:select #'ignore
:ttl 5)
"Default properties for popup rules defined with `set-popup-rule!'.")
;;;###autoload
(defun +popup-make-rule (predicate plist)
(if (plist-get plist :ignore)
(list predicate nil)
(let* ((plist (append plist +popup-defaults))
(alist
`((actions . ,(plist-get plist :actions))
(side . ,(plist-get plist :side))
(size . ,(plist-get plist :size))
(window-width . ,(plist-get plist :width))
(window-height . ,(plist-get plist :height))
(slot . ,(plist-get plist :slot))
(vslot . ,(plist-get plist :vslot))))
(params
`((ttl . ,(plist-get plist :ttl))
(quit . ,(plist-get plist :quit))
(select . ,(plist-get plist :select))
(modeline . ,(plist-get plist :modeline))
(autosave . ,(plist-get plist :autosave))
,@(plist-get plist :parameters))))
`(,predicate (+popup-buffer)
,@alist
(window-parameters ,@params)))))
;;;###autodef
(defun set-popup-rule! (predicate &rest plist)
"Define a popup rule.
These rules affect buffers displayed with `pop-to-buffer' and `display-buffer'
(or their siblings). Buffers displayed with `switch-to-buffer' (and its
variants) will not be affected by these rules (as they are unaffected by
`display-buffer-alist', which powers the popup management system).
PREDICATE can be either a) a regexp string (matched against the buffer's name)
or b) a function that takes two arguments (a buffer name and the ACTION argument
of `display-buffer') and returns a boolean.
PLIST can be made up of any of the following properties:
:ignore BOOL
If BOOL is non-nil, popups matching PREDICATE will not be handled by the popup
system. Use this for buffers that have their own window management system like
magit or helm.
:actions ACTIONS
ACTIONS is a list of functions or an alist containing (FUNCTION . ALIST). See
`display-buffer''s second argument for more information on its format and what
it accepts. If omitted, `+popup-default-display-buffer-actions' is used.
:side 'bottom|'top|'left|'right
Which side of the frame to open the popup on. This is only respected if
`+popup-display-buffer-stacked-side-window-fn' or `display-buffer-in-side-window'
is in :actions or `+popup-default-display-buffer-actions'.
:size/:width/:height FLOAT|INT|FN
Determines the size of the popup. If more tha one of these size properties are
given :size always takes precedence, and is mapped with window-width or
window-height depending on what :side the popup is opened. Setting a height
for a popup that opens on the left or right is harmless, but comes into play
if two popups occupy the same :vslot.
If a FLOAT (0 < x < 1), the number represents how much of the window will be
consumed by the popup (a percentage).
If an INT, the number determines the size in lines (height) or units of
character width (width).
If a function, it takes one argument: the popup window, and can do whatever it
wants with it, typically resize it, like `+popup-shrink-to-fit'.
:slot/:vslot INT
(This only applies to popups with a :side and only if :actions is blank or
contains the `+popup-display-buffer-stacked-side-window-fn' action) These control
how multiple popups are laid out. INT can be any integer, positive and
negative.
:slot controls lateral positioning (e.g. the horizontal positioning for
top/bottom popups, or vertical positioning for left/right popups).
:vslot controls popup stacking (from the edge of the frame toward the center).
Let's assume popup A and B are opened with :side 'bottom, in that order.
If they possess the same :slot and :vslot, popup B will replace popup A.
If popup B has a higher :slot, it will open to the right of popup A.
If popup B has a lower :slot, it will open to the left of popup A.
If popup B has a higher :vslot, it will open above popup A.
If popup B has a lower :vslot, it will open below popup A.
:ttl INT|BOOL|FN
Stands for time-to-live. It can be t, an integer, nil or a function. This
controls how (and if) the popup system will clean up after the popup.
If any non-zero integer, wait that many seconds before killing the buffer (and
any associated processes).
If 0, the buffer is immediately killed.
If nil, the buffer won't be killed and is left to its own devices.
If t, resort to the default :ttl in `+popup-defaults'. If none exists, this is
the same as nil.
If a function, it takes one argument: the target popup buffer. The popup
system does nothing else and ignores the function's return value.
:quit FN|BOOL|'other|'current
Can be t, 'other, 'current, nil, or a function. This determines the behavior
of the ESC/C-g keys in or outside of popup windows.
If t, close the popup if ESC/C-g is pressed anywhere.
If 'other, close this popup if ESC/C-g is pressed outside of any popup. This
is great for popups you may press ESC/C-g a lot in.
If 'current, close the current popup if ESC/C-g is pressed from inside of the
popup. This makes it harder to accidentally close a popup until you really
want to.
If nil, pressing ESC/C-g will never close this popup.
If a function, it takes one argument: the to-be-closed popup window, and is
run when ESC/C-g is pressed while that popup is open. It must return one of
the other values to determine the fate of the popup.
:select BOOL|FN
Can be a boolean or function. The boolean determines whether to focus the
popup window after it opens (non-nil) or focus the origin window (nil).
If a function, it takes two arguments: the popup window and originating window
(where you were before the popup opened). The popup system does nothing else
and ignores the function's return value.
:modeline BOOL|FN|LIST
Can be t (show the default modeline), nil (show no modeline), a function that
returns a modeline format or a valid value for `mode-line-format' to be used
verbatim. The function takes no arguments and is run in the context of the
popup buffer.
:autosave BOOL|FN
This parameter determines what to do with modified buffers when closing popup
windows. It accepts t, 'ignore, a function or nil.
If t, no prompts. Just save them automatically (if they're file-visiting
buffers). Same as 'ignore for non-file-visiting buffers.
If nil (the default), prompt the user what to do if the buffer is
file-visiting and modified.
If 'ignore, no prompts, no saving. Just silently kill it.
If a function, it is run with one argument: the popup buffer, and must return
non-nil to save or nil to do nothing (but no prompts).
:parameters ALIST
An alist of custom window parameters. See `(elisp)Window Parameters'.
If any of these are omitted, defaults derived from `+popup-defaults' will be
used.
\(fn PREDICATE &key IGNORE ACTIONS SIDE SIZE WIDTH HEIGHT SLOT VSLOT TTL QUIT SELECT MODELINE AUTOSAVE PARAMETERS)"
(declare (indent defun))
(push (+popup-make-rule predicate plist) +popup--display-buffer-alist)
(when (bound-and-true-p +popup-mode)
(setq display-buffer-alist +popup--display-buffer-alist))
+popup--display-buffer-alist)
;;;###autodef
(defun set-popup-rules! (&rest rulesets)
"Defines multiple popup rules.
Every entry in RULESETS should be a list of alists where the CAR is the
predicate and CDR is a plist. See `set-popup-rule!' for details on the predicate
and plist.
Example:
(set-popup-rules!
'((\"^ \\*\" :slot 1 :vslot -1 :size #'+popup-shrink-to-fit)
(\"^\\*\" :slot 1 :vslot -1 :select t))
'((\"^\\*Completions\" :slot -1 :vslot -2 :ttl 0)
(\"^\\*Compil\\(?:ation\\|e-Log\\)\" :size 0.3 :ttl 0 :quit t)))"
(declare (indent 0))
(dolist (rules rulesets)
(dolist (rule rules)
(push (+popup-make-rule (car rule) (cdr rule))
+popup--display-buffer-alist)))
(when (bound-and-true-p +popup-mode)
(setq display-buffer-alist +popup--display-buffer-alist))
+popup--display-buffer-alist)

View File

@@ -0,0 +1,175 @@
;;; ui/popup/config.el -*- lexical-binding: t; -*-
(defconst +popup-window-parameters '(ttl quit select modeline popup)
"A list of custom parameters to be added to `window-persistent-parameters'.
Modifying this has no effect, unless done before ui/popup loads.")
(defvar +popup-default-display-buffer-actions
'(+popup-display-buffer-stacked-side-window-fn)
"The functions to use to display the popup buffer.")
(defvar +popup-default-alist
'((window-height . 0.16) ; remove later
(reusable-frames . visible))
"The default alist for `display-buffer-alist' rules.")
(defvar +popup-default-parameters
'((transient . t) ; remove later
(quit . t) ; remove later
(select . ignore) ; remove later
(no-other-window . t))
"The default window parameters.")
(defvar +popup-margin-width 1
"Size of the margins to give popup windows. Set this to nil to disable margin
adjustment.")
(defvar +popup--inhibit-transient nil)
(defvar +popup--inhibit-select nil)
(defvar +popup--old-display-buffer-alist nil)
(defvar +popup--remember-last t)
(defvar +popup--last nil)
(defvar-local +popup--timer nil)
;;
;; Global modes
(defvar +popup-mode-map (make-sparse-keymap)
"Active keymap in a session with the popup system enabled. See
`+popup-mode'.")
(defvar +popup-buffer-mode-map
(let ((map (make-sparse-keymap)))
(when (featurep! :editor evil)
;; For maximum escape coverage in emacs state buffers; this only works in
;; GUI Emacs, in tty Emacs use C-g instead
(define-key map [escape] #'doom/escape))
map)
"Active keymap in popup windows. See `+popup-buffer-mode'.")
(define-minor-mode +popup-mode
"Global minor mode representing Doom's popup management system."
:init-value nil
:global t
:keymap +popup-mode-map
(cond (+popup-mode
(add-hook 'doom-escape-hook #'+popup-close-on-escape-h 'append)
(setq +popup--old-display-buffer-alist display-buffer-alist
display-buffer-alist +popup--display-buffer-alist
window--sides-inhibit-check t)
(dolist (prop +popup-window-parameters)
(push (cons prop 'writable) window-persistent-parameters)))
(t
(remove-hook 'doom-escape-hook #'+popup-close-on-escape-h)
(setq display-buffer-alist +popup--old-display-buffer-alist
window--sides-inhibit-check nil)
(+popup-cleanup-rules-h)
(dolist (prop +popup-window-parameters)
(delq (assq prop window-persistent-parameters)
window-persistent-parameters)))))
(define-minor-mode +popup-buffer-mode
"Minor mode for individual popup windows.
It is enabled when a buffer is displayed in a popup window and disabled when
that window has been changed or closed."
:init-value nil
:keymap +popup-buffer-mode-map
(if (not +popup-buffer-mode)
(remove-hook 'after-change-major-mode-hook #'+popup-set-modeline-on-enable-h t)
(add-hook 'after-change-major-mode-hook #'+popup-set-modeline-on-enable-h
nil 'local)
(when (timerp +popup--timer)
(remove-hook 'kill-buffer-hook #'+popup-kill-buffer-hook-h t)
(cancel-timer +popup--timer)
(setq +popup--timer nil))))
(put '+popup-buffer-mode 'permanent-local t)
(put '+popup-buffer-mode 'permanent-local-hook t)
(put '+popup-set-modeline-on-enable-h 'permanent-local-hook t)
;;
;; Macros
(defmacro with-popup-rules! (rules &rest body)
"Evaluate BODY with popup RULES. RULES is a list of popup rules. Each rule
should match the arguments of `+popup-define' or the :popup setting."
(declare (indent defun))
`(let ((+popup--display-buffer-alist +popup--old-display-buffer-alist)
display-buffer-alist)
(set-popup-rules! ,rules)
(when (bound-and-true-p +popup-mode)
(setq display-buffer-alist +popup--display-buffer-alist))
,@body))
(defmacro save-popups! (&rest body)
"Sets aside all popups before executing the original function, usually to
prevent the popup(s) from messing up the UI (or vice versa)."
`(let* ((in-popup-p (+popup-buffer-p))
(popups (+popup-windows))
(+popup--inhibit-transient t)
+popup--last)
(dolist (p popups)
(+popup/close p 'force))
(unwind-protect
(progn ,@body)
(when popups
(let ((origin (selected-window)))
(+popup/restore)
(unless in-popup-p
(select-window origin)))))))
;;
;; Default popup rules & bootstrap
(set-popup-rules!
(when (featurep! +all)
'(("^\\*" :slot 1 :vslot -1 :select t)
("^ \\*" :slot 1 :vslot -1 :size +popup-shrink-to-fit)))
(when (featurep! +defaults)
'(("^\\*Completions" :ignore t)
("^\\*\\(?:[Cc]ompil\\(?:ation\\|e-Log\\)\\|Messages\\)"
:vslot -2 :size 0.3 :autosave t :quit t :ttl nil)
("^\\*\\(?:doom \\|Pp E\\)" ; transient buffers (no interaction required)
:vslot -3 :size +popup-shrink-to-fit :autosave t :select ignore :quit t :ttl 0)
("^\\*doom:" ; editing buffers (interaction required)
:vslot -4 :size 0.35 :autosave t :select t :modeline t :quit nil :ttl t)
("^\\*doom:\\(?:v?term\\|eshell\\)-popup" ; editing buffers (interaction required)
:vslot -5 :size 0.35 :select t :modeline t :quit nil :ttl nil)
("^\\*\\(?:Wo\\)?Man "
:vslot -6 :size 0.45 :select t :quit t :ttl 0)
("^\\*Calc"
:vslot -7 :side bottom :size 0.4 :select t :quit nil :ttl 0)
("^\\*Customize"
:slot 2 :side right :select t :quit t)
("^ \\*undo-tree\\*"
:slot 2 :side left :size 20 :select t :quit t)
;; `help-mode', `helpful-mode'
("^\\*[Hh]elp"
:slot 2 :vslot -8 :size 0.35 :select t)
("^\\*eww\\*" ; `eww' (and used by dash docsets)
:vslot -11 :size 0.35 :select t)
("^\\*info\\*$" ; `Info-mode'
:slot 2 :vslot 2 :size 0.45 :select t)))
'(("^\\*Warnings" :vslot 99 :size 0.25)
("^\\*Backtrace" :vslot 99 :size 0.4 :quit nil)
("^\\*CPU-Profiler-Report " :side bottom :vslot 100 :slot 1 :height 0.4 :width 0.5 :quit nil)
("^\\*Memory-Profiler-Report " :side bottom :vslot 100 :slot 2 :height 0.4 :width 0.5 :quit nil)
("^\\*\\(?:Proced\\|timer-list\\|Process List\\|Abbrevs\\|Output\\|Occur\\|unsent mail\\)\\*" :ignore t)))
(add-hook 'doom-init-ui-hook #'+popup-mode 'append)
(add-hook! '+popup-buffer-mode-hook
#'+popup-adjust-fringes-h
#'+popup-adjust-margins-h
#'+popup-set-modeline-on-enable-h
#'+popup-unset-modeline-on-disable-h)
;;
;; Hacks
(load! "+hacks")

View File

@@ -0,0 +1,212 @@
;; -*- no-byte-compile: t; -*-
;;; ui/popup/test/test-popup.el
(require! :ui popup)
(describe "ui/popup"
:var (display-buffer-alist
+popup-default-display-buffer-actions
+popup--display-buffer-alist
+popup-defaults
wconf)
(before-all
(delete-other-windows)
(switch-to-buffer "*scratch*")
(setq wconf (current-window-configuration))
(+popup-mode +1))
(after-all
(+popup-mode -1))
(before-each
(setq display-buffer-alist nil
+popup--display-buffer-alist nil
+popup-default-display-buffer-actions '(+popup-display-buffer-stacked-side-window-fn)
+popup-defaults '(:side bottom :select ignore :ttl nil :slot 1 :vslot 1)))
(after-each
(set-window-configuration wconf))
(describe "set-popup-rule!"
(it "sets popup rules"
(set-popup-rule! "does-not-exist" :size 10)
(let ((rule (cdr (assoc "does-not-exist" display-buffer-alist))))
(expect rule :to-contain '(+popup-buffer))
(expect rule :to-contain '(size . 10))))
(it "shadows old rules"
(set-popup-rule! "a" :size 10)
(set-popup-rule! "a" :size 20)
(expect (cdr (assoc "a" display-buffer-alist))
:to-contain '(size . 20)))
(it "resolves to defaults"
(let ((+popup-defaults '(:size 5)))
(set-popup-rule! "a")
(expect (cdr (assoc "a" display-buffer-alist))
:to-contain '(size . 5)))))
(describe "popup rules"
:var (origin a b c d e f g)
(before-all (setq origin (current-buffer)))
(before-each
(dolist (name '(a b c d e f g))
(set name (get-buffer-create (symbol-name name)))))
(after-each
(let (kill-buffer-query-functions kill-buffer-hook)
(dolist (x (list a b c d e f g))
(ignore-errors (delete-window (get-buffer-window x)))
(kill-buffer x))))
(describe "slot positioning"
(before-each
(set-popup-rules!
'(("a" :slot 1 :vslot 1)
("b" :slot 2 :vslot 1)
("c" :slot 1 :vslot 2)
("d" :slot 2 :vslot 2)
("e" :slot 1 :vslot 3)
("f" :slot 1 :vslot 3)
("g"))))
(it "replaces popups with the same slots"
(mapc #'display-buffer (list e f))
(expect (length (+popup-windows)) :to-be 1))
(it "replaces popups among multiple that have the same slots"
(let ((first (display-buffer a))
(second (display-buffer b))
(third (display-buffer e))
(fourth (display-buffer f)))
(expect (+popup-windows) :to-have-same-items-as
(list first second fourth))))
(describe ":slot"
(it "opens left of others if lower"
(let ((first (display-buffer b))
(second (display-buffer a)))
(expect (length (+popup-windows)) :to-be 2)
(expect (window-in-direction 'left first t)
:to-equal second)))
(it "opens right of others if higher"
(let ((first (display-buffer a))
(second (display-buffer b)))
(expect (length (+popup-windows)) :to-be 2)
(expect (window-in-direction 'right first t)
:to-equal second)))
(it "obeys default :slot"
(let ((window (display-buffer g)))
(expect (window-parameter window 'window-slot) :to-be 1)
(expect (window-parameter window 'window-vslot) :to-be 1))))
(describe ":vslot"
;; TODO Implement this, somehow
(xit "opens lower :vslot popups above others"
(let ((first (display-buffer c))
(second (display-buffer a)))
(expect (length (+popup-windows)) :to-be 2)
(expect (window-in-direction 'above first t)
:to-equal second)))
(it "opens higher :vslot popups below others"
(let ((first (display-buffer c))
(second (display-buffer e)))
(expect (length (+popup-windows)) :to-be 2)
(expect (window-in-direction 'below first t)
:to-equal second)))))
(describe ":select"
(it "selects the popup if non-nil"
(set-popup-rule! "^a$" :select t)
(display-buffer a)
(expect (current-buffer) :to-equal a))
(it "selects the originating window if nil"
(set-popup-rule! "^a$" :select nil)
(display-buffer a)
(expect (current-buffer) :to-equal origin))
(it "fall back to base selection if passed #'ignore"
(spy-on 'ignore)
(set-popup-rule! "^a$" :select #'ignore)
(save-window-excursion
(display-buffer a)
(expect (current-buffer) :to-equal origin))
(save-window-excursion
(pop-to-buffer a)
(expect (current-buffer) :to-equal a))
(expect 'ignore :to-have-been-called-times 2)))
(describe ":modeline"
(it "disables the mode-line if nil"
(set-popup-rule! "a" :modeline nil :select t)
(display-buffer a)
(expect mode-line-format :to-be nil))
(it "uses the default mode-line if t"
(set-popup-rule! "a" :modeline t :select t)
(display-buffer a)
(expect mode-line-format :to-equal (default-value 'mode-line-format)))
(it "uses a predefined mode-line if passed a symbol"
(set-popup-rule! "a" :modeline '("x") :select t)
(display-buffer a)
(expect mode-line-format :to-equal '("x")))
(it "runs the handler if passed a function"
(set-popup-rule! "a" :modeline (lambda () (setq mode-line-format '("x"))) :select t)
(display-buffer a)
(expect mode-line-format :to-equal '("x"))))
;; TODO
(xdescribe ":autosave")
(describe ":quit"
(it "will close from anywhere if :quit = t"
(set-popup-rule! "a" :quit t)
(save-window-excursion
(display-buffer a)
(call-interactively #'+popup/close-all)
(expect (get-buffer-window a) :to-be nil))
(save-window-excursion
(pop-to-buffer a)
(call-interactively #'+popup/close)
(expect (get-buffer-window a) :to-be nil)))
(it "will only close from outside if :quit = 'other"
(set-popup-rule! "a" :quit 'other)
(save-window-excursion
(display-buffer a)
(call-interactively #'+popup/close-all)
(expect (get-buffer-window a) :to-be nil))
(save-window-excursion
(pop-to-buffer a)
(call-interactively #'+popup/close)
(expect (get-buffer-window a))))
(it "will only close from inside if :quit = 'current"
(set-popup-rule! "a" :quit 'current)
(save-window-excursion
(display-buffer a)
(call-interactively #'+popup/close-all)
(expect (get-buffer-window a)))
(save-window-excursion
(pop-to-buffer a)
(call-interactively #'+popup/close)
(expect (get-buffer-window a) :to-be nil)))
(it "never close a if :quit = nil"
(set-popup-rule! "a" :quit nil)
(save-window-excursion
(display-buffer a)
(call-interactively #'+popup/close-all)
(expect (get-buffer-window a)))
(save-window-excursion
(pop-to-buffer a)
(call-interactively #'+popup/close)
(expect (get-buffer-window a)))))
;; TODO
(xdescribe ":ttl")
(xdescribe ":size")
(xdescribe ":width")
(xdescribe ":height")
(xdescribe ":side")
(xdescribe ":actions"))
;; TODO
(xdescribe "predicate functions"
(describe "buffer-p")
(describe "window-p"))
;; TODO
(xdescribe "save-popups!")
(xdescribe "with-popup-rules!"))

View File

@@ -0,0 +1,122 @@
;;; ui/pretty-code/+fira.el -*- lexical-binding: t; -*-
(defvar +pretty-code-fira-code-font-name "Fira Code Symbol"
"Name of the fira code ligature font.")
(defvar +pretty-code-fira-code-font-ligatures
'(("www" . #Xe100)
("**" . #Xe101)
("***" . #Xe102)
("**/" . #Xe103)
("*>" . #Xe104)
("*/" . #Xe105)
("\\\\" . #Xe106)
("\\\\\\" . #Xe107)
("{-" . #Xe108)
("[]" . #Xe109)
("::" . #Xe10a)
(":::" . #Xe10b)
(":=" . #Xe10c)
("!!" . #Xe10d)
("!=" . #Xe10e)
("!==" . #Xe10f)
("-}" . #Xe110)
("--" . #Xe111)
("---" . #Xe112)
("-->" . #Xe113)
("->" . #Xe114)
("->>" . #Xe115)
("-<" . #Xe116)
("-<<" . #Xe117)
("-~" . #Xe118)
("#{" . #Xe119)
("#[" . #Xe11a)
("##" . #Xe11b)
("###" . #Xe11c)
("####" . #Xe11d)
("#(" . #Xe11e)
("#?" . #Xe11f)
("#_" . #Xe120)
("#_(" . #Xe121)
(".-" . #Xe122)
(".=" . #Xe123)
(".." . #Xe124)
("..<" . #Xe125)
("..." . #Xe126)
("?=" . #Xe127)
("??" . #Xe128)
(";;" . #Xe129)
("/*" . #Xe12a)
("/**" . #Xe12b)
("/=" . #Xe12c)
("/==" . #Xe12d)
("/>" . #Xe12e)
("//" . #Xe12f)
("///" . #Xe130)
("&&" . #Xe131)
("||" . #Xe132)
("||=" . #Xe133)
("|=" . #Xe134)
("|>" . #Xe135)
("^=" . #Xe136)
("$>" . #Xe137)
("++" . #Xe138)
("+++" . #Xe139)
("+>" . #Xe13a)
("=:=" . #Xe13b)
("==" . #Xe13c)
("===" . #Xe13d)
("==>" . #Xe13e)
("=>" . #Xe13f)
("=>>" . #Xe140)
("=<" . #Xe141)
("=<<" . #Xe142)
("=/=" . #Xe143)
(">-" . #Xe144)
(">=" . #Xe145)
(">=>" . #Xe146)
(">>" . #Xe147)
(">>-" . #Xe148)
(">>=" . #Xe149)
(">>>" . #Xe14a)
("<*" . #Xe14b)
("<*>" . #Xe14c)
("<|" . #Xe14d)
("<|>" . #Xe14e)
("<$" . #Xe14f)
("<$>" . #Xe150)
("<!--" . #Xe151)
("<-" . #Xe152)
("<--" . #Xe153)
("<->" . #Xe154)
("<+" . #Xe155)
("<+>" . #Xe156)
("<=" . #Xe157)
("<==" . #Xe158)
("<=>" . #Xe159)
("<=<" . #Xe15a)
("<>" . #Xe15b)
("<<" . #Xe15c)
("<<-" . #Xe15d)
("<<=" . #Xe15e)
("<<<" . #Xe15f)
("<~" . #Xe160)
("<~~" . #Xe161)
("</" . #Xe162)
("</>" . #Xe163)
("~@" . #Xe164)
("~-" . #Xe165)
("~=" . #Xe166)
("~>" . #Xe167)
("~~" . #Xe168)
("~~>" . #Xe169)
("%%" . #Xe16a)))
(defun +pretty-code-setup-fira-ligatures-h ()
(set-fontset-font t '(#Xe100 . #Xe16f) +pretty-code-fira-code-font-name)
(setq-default prettify-symbols-alist
(append prettify-symbols-alist
(mapcar #'+pretty-code--correct-symbol-bounds
+pretty-code-fira-code-font-ligatures))))
(add-hook 'doom-init-ui-hook #'+pretty-code-setup-fira-ligatures-h)

View File

@@ -0,0 +1,58 @@
;;; ui/pretty-code/+hasklig.el -*- lexical-binding: t; -*-
(defvar +pretty-code-hasklig-font-name "Hasklig"
"Name of the hasklig ligature font.")
(defvar +pretty-code-hasklig-font-ligatures
'(("&&" . #Xe100)
("***" . #Xe101)
("*>" . #Xe102)
("\\\\" . #Xe103)
("||" . #Xe104)
("|>" . #Xe105)
("::" . #Xe106)
("==" . #Xe107)
("===" . #Xe108)
("==>" . #Xe109)
("=>" . #Xe10a)
("=<<" . #Xe10b)
("!!" . #Xe10c)
(">>" . #Xe10d)
(">>=" . #Xe10e)
(">>>" . #Xe10f)
(">>-" . #Xe110)
(">-" . #Xe111)
("->" . #Xe112)
("-<" . #Xe113)
("-<<" . #Xe114)
("<*" . #Xe115)
("<*>" . #Xe116)
("<|" . #Xe117)
("<|>" . #Xe118)
("<$>" . #Xe119)
("<>" . #Xe11a)
("<-" . #Xe11b)
("<<" . #Xe11c)
("<<<" . #Xe11d)
("<+>" . #Xe11e)
(".." . #Xe11f)
("..." . #Xe120)
("++" . #Xe121)
("+++" . #Xe122)
("/=" . #Xe123)
(":::" . #Xe124)
(">=>" . #Xe125)
("->>" . #Xe126)
("<=>" . #Xe127)
("<=<" . #Xe128)
("<->" . #Xe129)))
(defun +pretty-code-setup-hasklig-ligatures-h ()
(set-fontset-font t '(#Xe100 . #Xe129) +pretty-code-hasklig-font-name)
(setq-default prettify-symbols-alist
(append prettify-symbols-alist
(mapcar #'+pretty-code--correct-symbol-bounds
+pretty-code-hasklig-font-ligatures))))
(add-hook 'doom-init-ui-hook #'+pretty-code-setup-hasklig-ligatures-h)

View File

@@ -0,0 +1,232 @@
;;; ui/pretty-code/+iosevka.el -*- lexical-binding: t; -*-
(defvar +pretty-code-iosevka-font-name "Iosevka"
"Name of the iosevka ligature font.")
(defvar +pretty-code-iosevka-font-ligatures
'(;; Double-ended hyphen arrows
("<->" . #Xe100)
("<-->" . #Xe101)
("<--->" . #Xe102)
("<---->" . #Xe103)
("<----->" . #Xe104)
;; Double-ended equals arrows
("<=>" . #Xe105)
("<==>" . #Xe106)
("<===>" . #Xe107)
("<====>" . #Xe108)
("<=====>" . #Xe109)
;; Double-ended asterisk operators
("<**>" . #Xe10a)
("<***>" . #Xe10b)
("<****>" . #Xe10c)
("<*****>" . #Xe10d)
;; HTML comments
("<!--" . #Xe10e)
("<!---" . #Xe10f)
;; Three-char ops with discards
("<$" . #Xe110)
("<$>" . #Xe111)
("$>" . #Xe112)
("<." . #Xe113)
("<.>" . #Xe114)
(".>" . #Xe115)
("<*" . #Xe116)
("<*>" . #Xe117)
("*>" . #Xe118)
("<\\" . #Xe119)
("<\\>" . #Xe11a)
("\\>" . #Xe11b)
("</" . #Xe11c)
("</>" . #Xe11d)
("/>" . #Xe11e)
("<\"" . #Xe11f)
("<\">" . #Xe120)
("\">" . #Xe121)
("<'" . #Xe122)
("<'>" . #Xe123)
("'>" . #Xe124)
("<^" . #Xe125)
("<^>" . #Xe126)
("^>" . #Xe127)
("<&" . #Xe128)
("<&>" . #Xe129)
("&>" . #Xe12a)
("<%" . #Xe12b)
("<%>" . #Xe12c)
("%>" . #Xe12d)
("<@" . #Xe12e)
("<@>" . #Xe12f)
("@>" . #Xe130)
("<#" . #Xe131)
("<#>" . #Xe132)
("#>" . #Xe133)
("<+" . #Xe134)
("<+>" . #Xe135)
("+>" . #Xe136)
("<-" . #Xe137)
("<->" . #Xe138)
("->" . #Xe139)
("<!" . #Xe13a)
("<!>" . #Xe13b)
("!>" . #Xe13c)
("<?" . #Xe13d)
("<?>" . #Xe13e)
("?>" . #Xe13f)
("<|" . #Xe140)
("<|>" . #Xe141)
("|>" . #Xe142)
("<:" . #Xe143)
("<:>" . #Xe144)
(":>" . #Xe145)
;; Colons
("::" . #Xe146)
(":::" . #Xe147)
("::::" . #Xe148)
;; Arrow-like operators
("->" . #Xe149)
("->-" . #Xe14a)
("->--" . #Xe14b)
("->>" . #Xe14c)
("->>-" . #Xe14d)
("->>--" . #Xe14e)
("->>>" . #Xe14f)
("->>>-" . #Xe150)
("->>>--" . #Xe151)
("-->" . #Xe152)
("-->-" . #Xe153)
("-->--" . #Xe154)
("-->>" . #Xe155)
("-->>-" . #Xe156)
("-->>--" . #Xe157)
("-->>>" . #Xe158)
("-->>>-" . #Xe159)
("-->>>--" . #Xe15a)
(">-" . #Xe15b)
(">--" . #Xe15c)
(">>-" . #Xe15d)
(">>--" . #Xe15e)
(">>>-" . #Xe15f)
(">>>--" . #Xe160)
("=>" . #Xe161)
("=>=" . #Xe162)
("=>==" . #Xe163)
("=>>" . #Xe164)
("=>>=" . #Xe165)
("=>>==" . #Xe166)
("=>>>" . #Xe167)
("=>>>=" . #Xe168)
("=>>>==" . #Xe169)
("==>" . #Xe16a)
("==>=" . #Xe16b)
("==>==" . #Xe16c)
("==>>" . #Xe16d)
("==>>=" . #Xe16e)
("==>>==" . #Xe16f)
("==>>>" . #Xe170)
("==>>>=" . #Xe171)
("==>>>==" . #Xe172)
(">=" . #Xe173)
(">==" . #Xe174)
(">>=" . #Xe175)
(">>==" . #Xe176)
(">>>=" . #Xe177)
(">>>==" . #Xe178)
("<-" . #Xe179)
("-<-" . #Xe17a)
("--<-" . #Xe17b)
("<<-" . #Xe17c)
("-<<-" . #Xe17d)
("--<<-" . #Xe17e)
("<<<-" . #Xe17f)
("-<<<-" . #Xe180)
("--<<<-" . #Xe181)
("<--" . #Xe182)
("-<--" . #Xe183)
("--<--" . #Xe184)
("<<--" . #Xe185)
("-<<--" . #Xe186)
("--<<--" . #Xe187)
("<<<--" . #Xe188)
("-<<<--" . #Xe189)
("--<<<--" . #Xe18a)
("-<" . #Xe18b)
("--<" . #Xe18c)
("-<<" . #Xe18d)
("--<<" . #Xe18e)
("-<<<" . #Xe18f)
("--<<<" . #Xe190)
("<=" . #Xe191)
("=<=" . #Xe192)
("==<=" . #Xe193)
("<<=" . #Xe194)
("=<<=" . #Xe195)
("==<<=" . #Xe196)
("<<<=" . #Xe197)
("=<<<=" . #Xe198)
("==<<<=" . #Xe199)
("<==" . #Xe19a)
("=<==" . #Xe19b)
("==<==" . #Xe19c)
("<<==" . #Xe19d)
("=<<==" . #Xe19e)
("==<<==" . #Xe19f)
("<<<==" . #Xe1a0)
("=<<<==" . #Xe1a1)
("==<<<==" . #Xe1a2)
("=<" . #Xe1a3)
("==<" . #Xe1a4)
("=<<" . #Xe1a5)
("==<<" . #Xe1a6)
("=<<<" . #Xe1a7)
("==<<<" . #Xe1a8)
;; Monadic operators
(">=>" . #Xe1a9)
(">->" . #Xe1aa)
(">-->" . #Xe1ab)
(">==>" . #Xe1ac)
("<=<" . #Xe1ad)
("<-<" . #Xe1ae)
("<--<" . #Xe1af)
("<==<" . #Xe1b0)
;; Composition operators
(">>" . #Xe1b1)
(">>>" . #Xe1b2)
("<<" . #Xe1b3)
("<<<" . #Xe1b4)
;; Lens operators
(":+" . #Xe1b5)
(":-" . #Xe1b6)
(":=" . #Xe1b7)
("+:" . #Xe1b8)
("-:" . #Xe1b9)
("=:" . #Xe1ba)
("=^" . #Xe1bb)
("=+" . #Xe1bc)
("=-" . #Xe1bd)
("=*" . #Xe1be)
("=/" . #Xe1bf)
("=%" . #Xe1c0)
("^=" . #Xe1c1)
("+=" . #Xe1c2)
("-=" . #Xe1c3)
("*=" . #Xe1c4)
("/=" . #Xe1c5)
("%=" . #Xe1c6)
;; Logical
("/\\" . #Xe1c7)
("\\/" . #Xe1c8)
;; Semigroup/monoid operators
("<>" . #Xe1c9)
("<+" . #Xe1ca)
("<+>" . #Xe1cb)
("+>" . #Xe1cc))
"Defines the character mappings for ligatures for Iosevka.")
(defun +pretty-code-setup-iosevka-ligatures-h ()
(set-fontset-font t '(#Xe100 . #Xe1cc) +pretty-code-iosevka-font-name)
(setq-default prettify-symbols-alist
(append prettify-symbols-alist
+pretty-code-iosevka-font-ligatures)))
(add-hook 'doom-init-ui-hook #'+pretty-code-setup-iosevka-ligatures-h)

View File

@@ -0,0 +1,259 @@
;;; ui/pretty-code/+pragmata-pro.el -*- lexical-binding: t; -*-
(defvar +pretty-code-pragmata-pro-font-name "PragmataPro"
"Name of the Pragmata Pro ligature font.")
(defvar +pretty-code-pragmata-pro-font-ligatures
'(("[ERROR]" . #XE2C0)
("[DEBUG]" . #XE2C1)
("[INFO]" . #XE2C2)
("[WARN]" . #XE2C3)
("[WARNING]" . #XE2C4)
("[ERR]" . #XE2C5)
("[FATAL]" . #XE2C6)
("[TRACE]" . #XE2C7)
("[FIXME]" . #XE2C8)
("[TODO]" . #XE2C9)
("[BUG]" . #XE2CA)
("[NOTE]" . #XE2CB)
("[HACK]" . #XE2CC)
("[MARK]" . #XE2CD)
("# ERROR" . #XE2F0)
("# DEBUG" . #XE2F1)
("# INFO" . #XE2F2)
("# WARN" . #XE2F3)
("# WARNING" . #XE2F4)
("# ERR" . #XE2F5)
("# FATAL" . #XE2F6)
("# TRACE" . #XE2F7)
("# FIXME" . #XE2F8)
("# TODO" . #XE2F9)
("# BUG" . #XE2FA)
("# NOTE" . #XE2FB)
("# HACK" . #XE2FC)
("# MARK" . #XE2FD)
("// ERROR" . #XE2E0)
("// DEBUG" . #XE2E1)
("// INFO" . #XE2E2)
("// WARN" . #XE2E3)
("// WARNING". #XE2E4)
("// ERR" . #XE2E5)
("// FATAL" . #XE2E6)
("// TRACE" . #XE2E7)
("// FIXME" . #XE2E8)
("// TODO" . #XE2E9)
("// BUG" . #XE2EA)
("// NOTE" . #XE2EB)
("// HACK" . #XE2EC)
("// MARK" . #XE2ED)
("!!" . #XE900)
("!=" . #XE901)
("!==" . #XE902)
("!!!" . #XE903)
("!≡" . #XE904)
("!≡≡" . #XE905)
("!>" . #XE906)
("!=<" . #XE907)
("#(" . #XE920)
("#_" . #XE921)
("#{" . #XE922)
("#?" . #XE923)
("#>" . #XE924)
("##" . #XE925)
("#_(" . #XE926)
("%=" . #XE930)
("%>" . #XE931)
("%>%" . #XE932)
("%<%" . #XE933)
("&%" . #XE940)
("&&" . #XE941)
("&*" . #XE942)
("&+" . #XE943)
("&-" . #XE944)
("&/" . #XE945)
("&=" . #XE946)
("&&&" . #XE947)
("&>" . #XE948)
("$>" . #XE955)
("***" . #XE960)
("*=" . #XE961)
("*/" . #XE962)
("*>" . #XE963)
("++" . #XE970)
("+++" . #XE971)
("+=" . #XE972)
("+>" . #XE973)
("++=" . #XE974)
("--" . #XE980)
("-<" . #XE981)
("-<<" . #XE982)
("-=" . #XE983)
("->" . #XE984)
("->>" . #XE985)
("---" . #XE986)
("-->" . #XE987)
("-+-" . #XE988)
("-\\/" . #XE989)
("-|>" . #XE98A)
("-<|" . #XE98B)
(".." . #XE990)
("..." . #XE991)
("..<" . #XE992)
(".>" . #XE993)
(".~" . #XE994)
(".=" . #XE995)
("/*" . #XE9A0)
("//" . #XE9A1)
("/>" . #XE9A2)
("/=" . #XE9A3)
("/==" . #XE9A4)
("///" . #XE9A5)
("/**" . #XE9A6)
(":::" . #XE9AF)
("::" . #XE9B0)
(":=" . #XE9B1)
(":≡" . #XE9B2)
(":>" . #XE9B3)
(":=>" . #XE9B4)
(":(" . #XE9B5)
(":-(" . #XE9B6)
(":)" . #XE9B7)
(":-)" . #XE9B8)
(":/" . #XE9B9)
(":\\" . #XE9BA)
(":3" . #XE9BB)
(":D" . #XE9BC)
(":P" . #XE9BD)
(":>:" . #XE9BE)
(":<:" . #XE9BF)
("<$>" . #XE9C0)
("<*" . #XE9C1)
("<*>" . #XE9C2)
("<+>" . #XE9C3)
("<-" . #XE9C4)
("<<" . #XE9C5)
("<<<" . #XE9C6)
("<<=" . #XE9C7)
("<=" . #XE9C8)
("<=>" . #XE9C9)
("<>" . #XE9CA)
("<|>" . #XE9CB)
("<<-" . #XE9CC)
("<|" . #XE9CD)
("<=<" . #XE9CE)
("<~" . #XE9CF)
("<~~" . #XE9D0)
("<<~" . #XE9D1)
("<$" . #XE9D2)
("<+" . #XE9D3)
("<!>" . #XE9D4)
("<@>" . #XE9D5)
("<#>" . #XE9D6)
("<%>" . #XE9D7)
("<^>" . #XE9D8)
("<&>" . #XE9D9)
("<?>" . #XE9DA)
("<.>" . #XE9DB)
("</>" . #XE9DC)
("<\\>" . #XE9DD)
("<\">" . #XE9DE)
("<:>" . #XE9DF)
("<~>" . #XE9E0)
("<**>" . #XE9E1)
("<<^" . #XE9E2)
("<!" . #XE9E3)
("<@" . #XE9E4)
("<#" . #XE9E5)
("<%" . #XE9E6)
("<^" . #XE9E7)
("<&" . #XE9E8)
("<?" . #XE9E9)
("<." . #XE9EA)
("</" . #XE9EB)
("<\\" . #XE9EC)
("<\"" . #XE9ED)
("<:" . #XE9EE)
("<->" . #XE9EF)
("<!--" . #XE9F0)
("<--" . #XE9F1)
("<~<" . #XE9F2)
("<==>" . #XE9F3)
("<|-" . #XE9F4)
("<<|" . #XE9F5)
("<-<" . #XE9F7)
("<-->" . #XE9F8)
("<<==" . #XE9F9)
("<==" . #XE9FA)
("=<<" . #XEA00)
("==" . #XEA01)
("===" . #XEA02)
("==>" . #XEA03)
("=>" . #XEA04)
("=~" . #XEA05)
("=>>" . #XEA06)
("=/=" . #XEA07)
("=~=" . #XEA08)
("==>>" . #XEA09)
("≡≡" . #XEA10)
("≡≡≡" . #XEA11)
("≡:≡" . #XEA12)
(">-" . #XEA20)
(">=" . #XEA21)
(">>" . #XEA22)
(">>-" . #XEA23)
(">>=" . #XEA24)
(">>>" . #XEA25)
(">=>" . #XEA26)
(">>^" . #XEA27)
(">>|" . #XEA28)
(">!=" . #XEA29)
(">->" . #XEA2A)
("??" . #XEA40)
("?~" . #XEA41)
("?=" . #XEA42)
("?>" . #XEA43)
("???" . #XEA44)
("?." . #XEA45)
("^=" . #XEA48)
("^." . #XEA49)
("^?" . #XEA4A)
("^.." . #XEA4B)
("^<<" . #XEA4C)
("^>>" . #XEA4D)
("^>" . #XEA4E)
("\\\\" . #XEA50)
("\\>" . #XEA51)
("\\/-" . #XEA52)
("@>" . #XEA57)
("|=" . #XEA60)
("||" . #XEA61)
("|>" . #XEA62)
("|||" . #XEA63)
("|+|" . #XEA64)
("|->" . #XEA65)
("|-->" . #XEA66)
("|=>" . #XEA67)
("|==>" . #XEA68)
("|>-" . #XEA69)
("|<<" . #XEA6A)
("||>" . #XEA6B)
("|>>" . #XEA6C)
("|-" . #XEA6D)
("||-" . #XEA6E)
("~=" . #XEA70)
("~>" . #XEA71)
("~~>" . #XEA72)
("~>>" . #XEA73)
("[[" . #XEA80)
("]]" . #XEA81)
("\">" . #XEA90)
("_|_" . #XEA97))
"Defines the character mappings for ligatures for Pragmata Pro.")
(defun +pretty-code-setup-pragmata-pro-ligatures-h ()
(setq-default prettify-symbols-alist
(append prettify-symbols-alist
(mapcar #'+pretty-code--correct-symbol-bounds
+pretty-code-pragmata-pro-font-ligatures))))
(add-hook 'doom-init-ui-hook #'+pretty-code-setup-pragmata-pro-ligatures-h)

View File

@@ -0,0 +1,55 @@
;;; ui/pretty-code/settings.el -*- lexical-binding: t; -*-
;;;###autoload
(defvar +pretty-code-symbols-alist '((t))
"An alist containing a mapping of major modes to its value for
`prettify-symbols-alist'.")
;;;###autodef
(defun set-pretty-symbols! (modes &rest plist)
"Associates string patterns with icons in certain major-modes.
MODES is a major mode symbol or a list of them.
PLIST is a property list whose keys must match keys in `+pretty-code-symbols',
and whose values are strings representing the text to be replaced with that
symbol. If the car of PLIST is nil, then unset any pretty symbols previously
defined for MODES.
The following properties are special:
:alist ALIST
Appends ALIST to `prettify-symbols-alist' literally, without mapping text to
`+pretty-code-symbols'.
:merge BOOL
If non-nil, merge with previously defined `prettify-symbols-alist',
otherwise overwrite it.
For example, the rule for emacs-lisp-mode is very simple:
(set-pretty-symbols! 'emacs-lisp-mode
:lambda \"lambda\")
This will replace any instances of \"lambda\" in emacs-lisp-mode with the symbol
assicated with :lambda in `+pretty-code-symbols'.
Pretty symbols can be unset for emacs-lisp-mode with:
(set-pretty-symbols! 'emacs-lisp-mode nil)"
(declare (indent defun))
(if (null (car-safe plist))
(dolist (mode (doom-enlist modes))
(delq (assq mode +pretty-code-symbols-alist)
+pretty-code-symbols-alist))
(let (results merge key)
(while plist
(pcase (setq key (pop plist))
(:merge (setq merge (pop plist)))
(:alist (setq results (append (pop plist) results)))
(_
(when-let (char (plist-get +pretty-code-symbols key))
(push (cons (pop plist) char) results)))))
(dolist (mode (doom-enlist modes))
(unless merge
(delq (assq mode +pretty-code-symbols-alist)
+pretty-code-symbols-alist))
(push (cons mode results) +pretty-code-symbols-alist)))))

View File

@@ -0,0 +1,93 @@
;;; ui/pretty-code/config.el -*- lexical-binding: t; -*-
(defvar +pretty-code-symbols
'(;; org
:name "»"
:src_block "»"
:src_block_end "«"
;; Functional
:lambda "λ"
:def "ƒ"
:composition ""
:map ""
;; Types
:null ""
:true "𝕋"
:false "𝔽"
:int ""
:float ""
:str "𝕊"
:bool "𝔹"
;; Flow
:not ""
:in ""
:not-in ""
:and ""
:or ""
:for ""
:some ""
:return ""
:yield ""
;; Other
:tuple ""
:pipe "" ;; FIXME: find a non-private char
:dot "")
"Options plist for `set-pretty-symbols!'.
This should not contain any symbols from the Unicode Private Area! There is no
universal way of getting the correct symbol as that area varies from font to
font.")
(defun +pretty-code--correct-symbol-bounds (ligature-alist)
"Prepend non-breaking spaces to a ligature.
This way `compose-region' (called by `prettify-symbols-mode') will use the
correct width of the symbols instead of the width measured by `char-width'."
(let ((len (length (car ligature-alist)))
(acc (list (cdr ligature-alist))))
(while (> len 1)
(setq acc (cons #X00a0 (cons '(Br . Bl) acc))
len (1- len)))
(cons (car ligature-alist) acc)))
(defvar +pretty-code-enabled-modes t
"List of major modes in which `prettify-symbols-mode' should be enabled.
If t, enable it everywhere. If the first element is 'not, enable it in any mode
besides what is listed.")
;; When you get to the right edge, it goes back to how it normally prints
(setq prettify-symbols-unprettify-at-point 'right-edge)
(defun +pretty-code-init-pretty-symbols-h ()
"Enable `prettify-symbols-mode'.
If in fundamental-mode, or a mode derived from special, comint, eshell or term
modes, this function does nothing.
Otherwise it builds `prettify-code-symbols-alist' according to
`+pretty-code-symbols-alist' for the current major-mode."
(unless (or (eq major-mode 'fundamental-mode)
(eq (get major-mode 'mode-class) 'special)
(derived-mode-p 'comint-mode 'eshell-mode 'term-mode))
(when (or (eq +pretty-code-enabled-modes t)
(if (eq (car +pretty-code-enabled-modes) 'not)
(not (memq major-mode (cdr +pretty-code-enabled-modes)))
(memq major-mode +pretty-code-enabled-modes)))
(setq prettify-symbols-alist
(append (cdr (assq major-mode +pretty-code-symbols-alist))
(default-value 'prettify-symbols-alist)))
(when prettify-symbols-mode
(prettify-symbols-mode -1))
(prettify-symbols-mode +1))))
(add-hook 'after-change-major-mode-hook #'+pretty-code-init-pretty-symbols-h)
;; Font-specific ligature support
(cond ((featurep! +fira)
(load! "+fira"))
((featurep! +iosevka)
(load! "+iosevka"))
((featurep! +hasklig)
(load! "+hasklig"))
((featurep! +pragmata-pro)
(load! "+pragmata-pro")))

View File

@@ -0,0 +1,18 @@
#+TITLE: ui/tabs
#+DATE: July 12, 2019
#+SINCE: v2.1
#+STARTUP: inlineimages
* Table of Contents :TOC_3:noexport:
- [[#description][Description]]
- [[#module-flags][Module Flags]]
- [[#plugins][Plugins]]
* Description
This module adds an Atom-esque tab bar to the Emacs UI.
** Module Flags
This module provides no flags.
** Plugins
+ [[https://github.com/ema2159/centaur-tabs][centaur-tabs]]

View File

@@ -0,0 +1,77 @@
;;; ui/tabs/autoload.el -*- lexical-binding: t; -*-
;;;###autoload
(defun +tabs-buffer-predicate (buffer)
"TODO"
(or (memq buffer (window-parameter nil 'tab-buffers))
(eq buffer (doom-fallback-buffer))))
;;
;;; Commands
;;;###autoload
(defun +tabs/close-tab-or-window ()
"TODO"
(interactive)
(call-interactively
(cond ((cdr (window-parameter nil 'tab-buffers))
#'kill-current-buffer)
((fboundp '+workspace/close-window-or-workspace)
#'+workspace/close-window-or-workspace)
(#'delete-window))))
;;
;;; Advice
;;;###autoload
(defun +tabs-kill-current-buffer-a (&rest _)
(+tabs-remove-buffer-h))
;;;###autoload
(defun +tabs-bury-buffer-a (orig-fn &rest args)
(if centaur-tabs-mode
(let ((b (current-buffer)))
(apply orig-fn args)
(unless (eq b (current-buffer))
(with-current-buffer b
(+tabs-remove-buffer-h))))
(apply orig-fn args)))
;;;###autoload
(defun +tabs-kill-tab-maybe-a (tab)
(let ((buffer (centaur-tabs-tab-value tab)))
(with-current-buffer buffer
;; `kill-current-buffer' is advised not to kill buffers visible in another
;; window, so it behaves better than `kill-buffer'.
(kill-current-buffer))
(centaur-tabs-display-update)))
;;
;;; Hooks
;;;###autoload
(defun +tabs-add-buffer-h ()
(when (and centaur-tabs-mode
(doom-real-buffer-p (current-buffer)))
(let* ((this-buf (current-buffer))
(buffers (window-parameter nil 'tab-buffers)))
(cl-pushnew this-buf buffers)
(add-hook 'kill-buffer-hook #'+tabs-remove-buffer-h nil t)
(set-window-parameter nil 'tab-buffers buffers))))
;;;###autoload
(defun +tabs-remove-buffer-h ()
(when centaur-tabs-mode
(set-window-parameter
nil
'tab-buffers (delete (current-buffer)
(window-parameter nil 'tab-buffers)))))
;;;###autoload
(defun +tabs-new-window-h ()
(when centaur-tabs-mode
(unless (window-parameter nil 'tab-buffers)
(+tabs-add-buffer-h))))

View File

@@ -0,0 +1,71 @@
;;; ui/tabs/config.el -*- lexical-binding: t; -*-
(use-package! centaur-tabs
:after-call after-find-file dired-initial-position-hook
:init
(setq centaur-tabs-height 28
centaur-tabs-set-bar 'left
centaur-tabs-set-modified-marker t)
:config
(add-hook! 'centaur-tabs-mode-hook
(defun +tabs-init-frames-h ()
(dolist (frame (frame-list))
(if (not centaur-tabs-mode)
(set-frame-parameter frame 'buffer-predicate (frame-parameter frame 'old-buffer-predicate))
(set-frame-parameter frame 'old-buffer-predicate (frame-parameter frame 'buffer-predicate))
(set-frame-parameter frame 'buffer-predicate #'+tabs-buffer-predicate)))))
(add-to-list 'window-persistent-parameters '(tab-buffers . t))
(defun +tabs-window-buffer-list-fn ()
(centaur-tabs-filter-out
'centaur-tabs-hide-tab-cached
(delq nil
(cl-mapcar #'(lambda (b)
(cond
;; Always include the current buffer.
((eq (current-buffer) b) b)
((buffer-file-name b) b)
((char-equal ?\ (aref (buffer-name b) 0)) nil)
((buffer-live-p b) b)))
(window-parameter nil 'tab-buffers)))))
(defun +tabs-buffer-groups-fn ()
(list
(cond ((or (string-equal "*" (substring (buffer-name) 0 1))
(memq major-mode '(magit-process-mode
magit-status-mode
magit-diff-mode
magit-log-mode
magit-file-mode
magit-blob-mode
magit-blame-mode
)))
"Emacs")
((derived-mode-p 'eshell-mode)
"EShell")
((derived-mode-p 'dired-mode)
"Dired")
((centaur-tabs-get-group-name (current-buffer))))))
(setq centaur-tabs-buffer-list-function #'+tabs-window-buffer-list-fn
centaur-tabs-buffer-groups-function #'+tabs-buffer-groups-fn)
(advice-add #'centaur-tabs-buffer-close-tab :override #'+tabs-kill-tab-maybe-a)
(advice-add #'bury-buffer :around #'+tabs-bury-buffer-a)
(advice-add #'kill-current-buffer :before #'+tabs-kill-current-buffer-a)
(add-hook 'doom-switch-buffer-hook #'+tabs-add-buffer-h)
(add-hook 'doom-switch-window-hook #'+tabs-new-window-h)
(add-hook '+doom-dashboard-mode-hook #'centaur-tabs-local-mode)
(map! (:map centaur-tabs-mode-map
[remap delete-window] #'+tabs/close-tab-or-window
[remap +workspace/close-window-or-workspace] #'+tabs/close-tab-or-window)
(:after persp-mode
:map persp-mode-map
[remap delete-window] #'+tabs/close-tab-or-window
[remap +workspace/close-window-or-workspace] #'+tabs/close-tab-or-window))
(centaur-tabs-mode +1))

View File

@@ -0,0 +1,4 @@
;; -*- no-byte-compile: t; -*-
;;; ui/tabs/packages.el
(package! centaur-tabs)

View File

@@ -0,0 +1,41 @@
;;; ui/treemacs/autoload.el -*- lexical-binding: t; -*-
(defun +treemacs--init ()
(require 'treemacs)
(let ((origin-buffer (current-buffer)))
(cl-letf (((symbol-function 'treemacs-workspace->is-empty?)
(symbol-function 'ignore)))
(treemacs--init))
(dolist (project (treemacs-workspace->projects (treemacs-current-workspace)))
(treemacs-do-remove-project-from-workspace project))
(with-current-buffer origin-buffer
(let ((project-root (or (doom-project-root) default-directory)))
(treemacs-do-add-project-to-workspace
(treemacs--canonical-path project-root)
(doom-project-name project-root)))
(setq treemacs--ready-to-follow t)
(when (or treemacs-follow-after-init treemacs-follow-mode)
(treemacs--follow)))))
;;;###autoload
(defun +treemacs/toggle ()
"Initialize or toggle treemacs.
Ensures that only the current project is present and all other projects have
been removed.
Use `treemacs' command for old functionality."
(interactive)
(require 'treemacs)
(pcase (treemacs-current-visibility)
(`visible (delete-window (treemacs-get-local-window)))
(_ (+treemacs--init))))
;;;###autoload
(defun +treemacs/find-file (arg)
"Open treemacs (if necessary) and find current file."
(interactive "P")
(let ((origin-buffer (current-buffer)))
(+treemacs--init)
(with-current-buffer origin-buffer
(treemacs-find-file arg))))

View File

@@ -0,0 +1,45 @@
;;; ui/treemacs/config.el -*- lexical-binding: t; -*-
(setq treemacs-follow-after-init t
treemacs-is-never-other-window t
treemacs-sorting 'alphabetic-case-insensitive-asc
treemacs-persist-file (concat doom-cache-dir "treemacs-persist")
treemacs-last-error-persist-file (concat doom-cache-dir "treemacs-last-error-persist"))
(after! treemacs
(set-popup-rule! "^ \\*Treemacs"
:side treemacs-position
:size treemacs-width
:quit nil
:ttl 0)
;; Don't follow the cursor
(treemacs-follow-mode -1)
;; Allow ace-window to target treemacs windows
(after! ace-window
(delq! 'treemacs-mode aw-ignored-buffers)))
(use-package! treemacs-evil
:when (featurep! :editor evil +everywhere)
:after treemacs
:config
(define-key! evil-treemacs-state-map
[return] #'treemacs-RET-action
[tab] #'treemacs-TAB-action
"TAB" #'treemacs-TAB-action
;; REVIEW Fix #1875 to be consistent with C-w {v,s}, but this should really
;; be considered upstream.
"o v" #'treemacs-visit-node-horizontal-split
"o s" #'treemacs-visit-node-vertical-split))
(use-package! treemacs-projectile
:after treemacs)
(use-package! treemacs-magit
:when (featurep! :tools magit)
:after treemacs magit)

View File

@@ -0,0 +1,9 @@
;; -*- no-byte-compile: t; -*-
;;; ui/treemacs/packages.el
(package! treemacs)
(when (featurep! :editor evil +everywhere)
(package! treemacs-evil))
(package! treemacs-projectile)
(when (featurep! :tools magit)
(package! treemacs-magit))

View File

@@ -0,0 +1,10 @@
#+TITLE: :ui unicode
This unicode extends Doom's ability to display non-English unicode.
This is for non-English Emacs users, for whom Doom's built-in unicode support in insufficient.
When this module is enabled...
+ The first time you run Emacs a unicode cache will be generated -- this will take a while!
+ Doom will ignore the ~doom-unicode-font~ variable and the ~:unicode-font~ setting.

View File

@@ -0,0 +1,21 @@
;;; ui/unicode/autoload.el -*- lexical-binding: t; -*-
;;;###autoload
(add-hook! 'doom-init-ui-hook
(defun +unicode-init-fonts-h ()
"Set up `unicode-fonts' to eventually run; accommodating the daemon, if
necessary."
(setq-default bidi-display-reordering t
doom-unicode-font nil)
(if initial-window-system
(+unicode-setup-fonts-h (selected-frame))
(add-hook 'after-make-frame-functions #'+unicode-setup-fonts-h))))
;;;###autoload
(defun +unicode-setup-fonts-h (&optional frame)
"Initialize `unicode-fonts', if in a GUI session."
(when (and frame (display-graphic-p frame))
(with-selected-frame frame
(require 'unicode-fonts)
;; NOTE will impact startup time on first run
(unicode-fonts-setup))))

View File

@@ -0,0 +1,4 @@
;; -*- no-byte-compile: t; -*-
;;; ui/unicode/packages.el
(package! unicode-fonts)

View File

@@ -0,0 +1,27 @@
;;; ui/vc-gutter/autoload.el -*- lexical-binding: t; -*-
;;;###if (featurep! :ui hydra)
;;;###autoload (autoload '+vc/gutter-hydra/body "ui/vc-gutter/autoload" nil t)
(defhydra +vc/gutter-hydra
(:body-pre (git-gutter-mode 1) :hint nil)
"
[git gutter]
Movement Hunk Actions Misc. +%-4s(car (git-gutter:statistic))/ -%-4s(cdr (git-gutter:statistic))
╭──────────────────────────────────┴────────────────╯
^_g_^ [_s_] stage [_R_] set start Rev
^_k_^ [_r_] revert
^↑ ^ [_m_] mark
^↓ ^ [_p_] popup ╭─────────────────────
^_j_^ │[_q_] quit
^_G_^ │[_Q_] Quit and disable"
("j" (progn (git-gutter:next-hunk 1) (recenter)))
("k" (progn (git-gutter:previous-hunk 1) (recenter)))
("g" (progn (goto-char (point-min)) (git-gutter:next-hunk 1)))
("G" (progn (goto-char (point-min)) (git-gutter:previous-hunk 1)))
("s" git-gutter:stage-hunk)
("r" git-gutter:revert-hunk)
("m" git-gutter:mark-hunk)
("p" git-gutter:popup-hunk)
("R" git-gutter:set-start-revision)
("q" nil :color blue)
("Q" (git-gutter-mode -1) :color blue))

View File

@@ -0,0 +1,111 @@
;;; ui/vc-gutter/config.el -*- lexical-binding: t; -*-
(defvar +vc-gutter-in-margin nil
"If non-nil, use the margin for diffs instead of the fringe.")
(defvar +vc-gutter-in-remote-files nil
"If non-nil, enable the vc gutter in remote files (e.g. open through TRAMP).")
(defvar +vc-gutter-diff-unsaved-buffer nil
"If non-nil, `diff-hl-flydiff-mode' will be activated. This allows on-the-fly
diffing, even for unsaved buffers.")
(defvar +vc-gutter-default-style t
"If non-nil, enable the default look of the vc gutter. This means subtle thin
bitmaps on the left, an arrow bitmap for flycheck, and flycheck indicators moved
to the right fringe.")
;;
;; Packages
(use-package! git-gutter
:commands git-gutter:revert-hunk git-gutter:stage-hunk
:init
(add-hook! 'find-file-hook
(defun +vc-gutter-init-maybe-h ()
"Enable `git-gutter-mode' in the current buffer.
If the buffer doesn't represent an existing file, `git-gutter-mode's activation
is deferred until the file is saved. Respects `git-gutter:disabled-modes'."
(when (or +vc-gutter-in-remote-files
(not (file-remote-p (or buffer-file-name default-directory))))
(if (not buffer-file-name)
(add-hook 'after-save-hook #'+vc-gutter-init-maybe-h nil 'local)
(when (and (vc-backend buffer-file-name)
(progn
(require 'git-gutter)
(not (memq major-mode git-gutter:disabled-modes))))
(if (and (display-graphic-p)
(require 'git-gutter-fringe nil t))
(progn
(setq-local git-gutter:init-function #'git-gutter-fr:init)
(setq-local git-gutter:view-diff-function #'git-gutter-fr:view-diff-infos)
(setq-local git-gutter:clear-function #'git-gutter-fr:clear)
(setq-local git-gutter:window-width -1))
(setq-local git-gutter:init-function 'nil)
(setq-local git-gutter:view-diff-function #'git-gutter:view-diff-infos)
(setq-local git-gutter:clear-function #'git-gutter:clear-diff-infos)
(setq-local git-gutter:window-width 1))
(git-gutter-mode +1)
(remove-hook 'after-save-hook #'+vc-gutter-init-maybe-h 'local))))))
;; Disable in Org mode, as per
;; <https://github.com/syl20bnr/spacemacs/issues/10555> and
;; <https://github.com/syohex/emacs-git-gutter/issues/24>. Apparently, the
;; mode-enabling function for global minor modes gets called for new buffers
;; while they are still in `fundamental-mode', before a major mode has been
;; assigned. I don't know why this is the case, but adding `fundamental-mode'
;; here fixes the issue.
(setq git-gutter:disabled-modes '(fundamental-mode image-mode pdf-view-mode))
;; standardize default fringe width
(if (fboundp 'fringe-mode) (fringe-mode '4))
:config
(set-popup-rule! "^\\*git-gutter" :select nil :size '+popup-shrink-to-fit)
;; Update git-gutter on focus (in case I was using git externally)
(add-hook 'focus-in-hook #'git-gutter:update-all-windows)
(add-hook! '(doom-escape-hook doom-switch-window-hook) :append
(defun +vc-gutter-update-h (&rest _)
"Refresh git-gutter on ESC. Return nil to prevent shadowing other
`doom-escape-hook' hooks."
(when (and git-gutter-mode
(not (memq this-command '(git-gutter:stage-hunk
git-gutter:revert-hunk))))
(ignore (git-gutter)))))
;; update git-gutter when using magit commands
(advice-add #'magit-stage-file :after #'+vc-gutter-update-h)
(advice-add #'magit-unstage-file :after #'+vc-gutter-update-h)
(defadvice! +vc-gutter--fix-linearity-of-hunks-a (diffinfos is-reverse)
"Fixes `git-gutter:next-hunk' and `git-gutter:previous-hunk' sometimes
jumping to random hunks."
:override #'git-gutter:search-near-diff-index
(cl-position-if (let ((lineno (line-number-at-pos)))
(lambda (line)
(funcall (if is-reverse #'> #'<) lineno line)))
diffinfos
:key #'git-gutter-hunk-start-line
:from-end is-reverse)))
;; subtle diff indicators in the fringe
(when +vc-gutter-default-style
(after! git-gutter-fringe
;; places the git gutter outside the margins.
(setq-default fringes-outside-margins t)
;; thin fringe bitmaps
(define-fringe-bitmap 'git-gutter-fr:added [224]
nil nil '(center repeated))
(define-fringe-bitmap 'git-gutter-fr:modified [224]
nil nil '(center repeated))
(define-fringe-bitmap 'git-gutter-fr:deleted [128 192 224 240]
nil nil 'bottom)
;; let diff have left fringe, flycheck can have right fringe
(after! flycheck
(setq flycheck-indication-mode 'right-fringe)
;; A non-descript, left-pointing arrow
(define-fringe-bitmap 'flycheck-fringe-bitmap-double-arrow
[16 48 112 240 112 48 16] nil nil 'center))))

View File

@@ -0,0 +1,4 @@
;; -*- no-byte-compile: t; -*-
;;; ui/vc-gutter/packages.el
(package! git-gutter-fringe)

View File

@@ -0,0 +1,5 @@
;;; ui/vi-tilde-fringe/autoload.el -*- lexical-binding: t; -*-
;;;###autoload
(add-hook! '(prog-mode-hook text-mode-hook conf-mode-hook)
#'vi-tilde-fringe-mode)

View File

@@ -0,0 +1,4 @@
;; -*- no-byte-compile: t; -*-
;;; ui/vi-tilde-fringe/packages.el
(package! vi-tilde-fringe)

View File

@@ -0,0 +1,57 @@
#+TITLE: ui/window-select
#+DATE: October 8, 2017
#+SINCE: v2.0.7
#+STARTUP: inlineimages
* Table of Contents :TOC:
- [[#description][Description]]
- [[#module-flags][Module Flags]]
- [[#packages][Packages]]
- [[#prerequisites][Prerequisites]]
- [[#features][Features]]
- [[#configuration][Configuration]]
- [[#ace-window][ace-window]]
- [[#switch-window][switch-window]]
* Description
This module provides several methods for selecting windows without the use of
the mouse or spatial navigation (e.g. =C-w {h,j,k,l}=).
The command ~other-window~ is remapped to either ~switch-window~ or
~ace-window~, depending on which backend you've enabled. It is bound to ~C-x o~
(and ~C-w C-w~ for evil users).
It also provides numbered windows and selection with the ~winum~ package, if
desired. Evil users can jump to window N in =C-w <N>= (where N is a number
between 0 and 9). Non evil users have =C-x w <N>= instead.
** Module Flags
+ =+switch-window= Use the switch-window package as the backend, instead of
ace-window (avy).
+ =+numbers= Enable numbered windows and window selection (using winum).
** Packages
+ [[https://github.com/dimitri/switch-window][switch-window]] (if =+switch-window=)
+ [[https://github.com/abo-abo/ace-window][ace-window]] (if =+switch-window= isn't enabled)
+ [[https://github.com/deb0ch/emacs-winum][winum]] (if =+numbers=)
* Prerequisites
This module has no additional dependencies.
* TODO Features
* Configuration
This module provides two backends, both providing the same functionality, but
with different visual cues. They are =ace-window= and =switch-window=.
** ace-window
The first character of the buffers changes to a highlighted, user-selectable
character.
+ Pros: the content of the buffers are always visible.
+ Cons: The displayed characters are small and difficult to see.
** switch-window
Replaces the entire buffer with large letters.
+ Pros: The displayed characters are /really/ easy to see.
+ Cons: You can't see the contents of the buffers.

View File

@@ -0,0 +1,39 @@
;;; ui/window-select/config.el -*- lexical-binding: t; -*-
(use-package! switch-window
:when (featurep! +switch-window)
:defer t
:init
(global-set-key [remap other-window] #'switch-window)
:config
(setq switch-window-shortcut-style 'qwerty
switch-window-qwerty-shortcuts '("a" "s" "d" "f" "g" "h" "j" "k" "l")))
(use-package! ace-window
:unless (featurep! +switch-window)
:defer t
:init
(global-set-key [remap other-window] #'ace-window)
:config
(setq aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l)
aw-scope 'frame
aw-background t))
(use-package! winum
:when (featurep! +numbers)
:after-call doom-switch-window-hook
:config
(winum-mode +1)
(map! :map evil-window-map
"0" #'winum-select-window-0-or-10
"1" #'winum-select-window-1
"2" #'winum-select-window-2
"3" #'winum-select-window-3
"4" #'winum-select-window-4
"5" #'winum-select-window-5
"6" #'winum-select-window-6
"7" #'winum-select-window-7
"8" #'winum-select-window-8
"9" #'winum-select-window-9))

View File

@@ -0,0 +1,9 @@
;; -*- no-byte-compile: t; -*-
;;; ui/window-select/packages.el
(if (featurep! +switch-window)
(package! switch-window)
(package! ace-window))
(when (featurep! +numbers)
(package! winum))

View File

@@ -0,0 +1,104 @@
#+TITLE: ui/workspaces
#+DATE: February 4, 2017
#+SINCE: v1.3
#+STARTUP: inlineimages
* Table of Contents :TOC:
- [[#description][Description]]
- [[#module-flags][Module Flags]]
- [[#packages][Packages]]
- [[#prerequisites][Prerequisites]]
- [[#features][Features]]
- [[#isolated-buffer-list][Isolated buffer-list]]
- [[#automatic-workspaces][Automatic workspaces]]
- [[#session-persistence][Session persistence]]
- [[#workspace-persistence][Workspace persistence]]
- [[#appendix][Appendix]]
- [[#commands--keybindings][Commands & Keybindings]]
- [[#api][API]]
* Description
This module adds support for workspaces, powered by persp_mode, as well as a API
for manipulating them.
#+begin_quote
There are many ways to use workspaces. I spawn a workspace per task. Say I'm
working in the main workspace, when I realize there is a bug in another part of
my project. I open a new workspace and deal with it in there. In the meantime, I
need to check my email, so mu4e gets its own workspace.
Once I've completed the task, I close the workspace and return to main.
#+end_quote
** Module Flags
This module provides no flags.
** Packages
+ [[https://github.com/Bad-ptr/persp-mode.el][persp-mode]]
* Prerequisites
This module has no additional dependencies.
* Features
** Isolated buffer-list
When persp-mode is active, ~doom-buffer-list~ becomes workspace-restricted. You
can overcome this by using ~buffer-list~.
** Automatic workspaces
A workspace is automatically created (and switched to) when you:
+ Create a new frame (with =make-frame=; bound to =M-N= by default).
+ Switch to a project using ~projectile-switch-project~.
** Session persistence
By default, your session is autosaved when you quit Emacs (or disable
~persp-mode~). You can load a previous session with ~M-x
+workspace/load-session~ or ~:sl[oad]~ (ex command).
You can supply either a name to load a specific session to replace your current
one.
** Workspace persistence
If you'd like to save a specific workspace, use ~M-x +workspace/save~, which can
be loaded into the current session (as another workspace) with ~M-x
+workspace/load~.
* Appendix
** Commands & Keybindings
Here is a list of available commands, their default keybindings (defined in
[[../../config/default/+evil-bindings.el][private/default/+bindings.el]]), and corresponding ex commands (if any -- defined
in [[../../editor/evil/+commands.el][private/default/+evil-commands.el]]).
| command | key / ex command | description |
|-----------------------------------+----------------------------+------------------------------------------------------------|
| ~+workspace/new~ | =SPC TAB n= | Create a new, blank workspace |
| ~+workspace/display~ | =SPC TAB TAB= | Display open workspaces in the mode-line |
| ~+workspace/load~ | =SPC TAB l= | Load a saved workspace into the current session |
| ~+workspace/restore-last-session~ | =SPC TAB R= | Restore last session |
| ~+workspace/rename~ | =SPC TAB r= | Rename the current workspace |
| ~+workspace/save~ | =SPC TAB s= | Save the current workspace to a file |
| ~+workspace/switch-to~ | =SPC TAB .= | Switch to an open workspace |
| ~+workspace/other~ | =SPC TAB `= | Switch to last workspace |
| ~+workspace/switch-left~ | =SPC TAB [= / =[ w= / =gT= | Switch to previous workspace |
| ~+workspace/switch-right~ | =SPC TAB ]= / =] w= / =gt= | Switch to next workspace |
| ~+workspace/delete~ | =SPC TAB d= | Delete the current workspace |
| ~+workspace/kill-session~ | =SPC TAB x= / =:sclear= | Clears the current session (kills all windows and buffers) |
** API
+ ~+workspace-list~ -> list<Struct>
+ ~+workspace-list-names~ -> list<string>
+ ~+workspace-buffer-list &optional PERSP~ -> bool
+ ~+workspace-p OBJ~ -> bool
+ ~+workspace-exists-p NAME~ -> bool
+ ~+workspace-get NAME &optional NOERROR~ -> Struct
+ ~+workspace-current &optional FRAME WINDOW~ -> Struct
+ ~+workspace-current-name~ -> string
+ ~+workspace-load NAME~
+ ~+workspace-load-session NAME~
+ ~+workspace-save NAME~
+ ~+workspace-save-session NAME~
+ ~+workspace-new NAME~
+ ~+workspace-rename NAME NEW-NAME~
+ ~+workspace-delete NAME &optional INHIBIT-KILL-P~
+ ~+workspace-switch NAME &optional AUTO-CREATE-P~
+ ~+workspace-protected-p NAME~ -> bool

View File

@@ -0,0 +1,39 @@
;;; ui/workspaces/autoload/evil.el -*- lexical-binding: t; -*-
;;;###if (featurep! :editor evil)
;;;###autoload (autoload '+workspace:save "ui/workspaces/autoload/evil" nil t)
(evil-define-command +workspace:save (&optional name)
"Ex wrapper around `+workspace/save-session'."
(interactive "<a>") (+workspace/save name))
;;;###autoload (autoload '+workspace:load "ui/workspaces/autoload/evil" nil t)
(evil-define-command +workspace:load (&optional name)
"Ex wrapper around `+workspace/load-session'."
(interactive "<a>") (+workspace/load name))
;;;###autoload (autoload '+workspace:new "ui/workspaces/autoload/evil" nil t)
(evil-define-command +workspace:new (bang name)
"Ex wrapper around `+workspace/new'. If BANG, clone the current workspace."
(interactive "<!><a>") (+workspace/new name bang))
;;;###autoload (autoload '+workspace:rename "ui/workspaces/autoload/evil" nil t)
(evil-define-command +workspace:rename (new-name)
"Ex wrapper around `+workspace/rename'."
(interactive "<a>") (+workspace/rename new-name))
;;;###autoload (autoload '+workspace:delete "ui/workspaces/autoload/evil" nil t)
(evil-define-command +workspace:delete ()
"Ex wrapper around `+workspace/delete'."
(interactive) (+workspace/delete (+workspace-current-name)))
;;;###autoload (autoload '+workspace:switch-next "ui/workspaces/autoload/evil" nil t)
(evil-define-command +workspace:switch-next (&optional count)
"Switch to next workspace. If COUNT, switch to COUNT-th workspace."
(interactive "<c>")
(if count (+workspace/switch-to count) (+workspace/cycle +1)))
;;;###autoload (autoload '+workspace:switch-previous "ui/workspaces/autoload/evil" nil t)
(evil-define-command +workspace:switch-previous (&optional count)
"Switch to previous workspace. If COUNT, switch to COUNT-th workspace."
(interactive "<c>")
(if count (+workspace/switch-to count) (+workspace/cycle -1)))

View File

@@ -0,0 +1,531 @@
;;; feature/workspaces/autoload/workspaces.el -*- lexical-binding: t; -*-
(defvar +workspace--last nil)
(defvar +workspace--index 0)
;;;###autoload
(defface +workspace-tab-selected-face '((t (:inherit highlight)))
"The face for selected tabs displayed by `+workspace/display'"
:group 'persp-mode)
;;;###autoload
(defface +workspace-tab-face '((t (:inherit default)))
"The face for selected tabs displayed by `+workspace/display'"
:group 'persp-mode)
;;
;;; Library
(defun +workspace--protected-p (name)
(equal name persp-nil-name))
(defun +workspace--generate-id ()
(or (cl-loop for name in (+workspace-list-names)
when (string-match-p "^#[0-9]+$" name)
maximize (string-to-number (substring name 1)) into max
finally return (if max (1+ max)))
1))
;;; Predicates
;;;###autoload
(defalias #'+workspace-p #'perspective-p
"Return t if OBJ is a perspective hash table.")
;;;###autoload
(defun +workspace-exists-p (name)
"Returns t if NAME is the name of an existing workspace."
(member name (+workspace-list-names)))
;;;###autoload
(defalias #'+workspace-contains-buffer-p #'persp-contain-buffer-p
"Return non-nil if BUFFER is in WORKSPACE (defaults to current workspace).")
;;; Getters
;;;###autoload
(defalias #'+workspace-current #'get-current-persp
"Return the currently active workspace.")
;;;###autoload
(defun +workspace-get (name &optional noerror)
"Return a workspace named NAME. Unless NOERROR is non-nil, this throws an
error if NAME doesn't exist."
(cl-check-type name string)
(when-let (persp (persp-get-by-name name))
(cond ((+workspace-p persp) persp)
((not noerror)
(error "No workspace called '%s' was found" name)))))
;;;###autoload
(defun +workspace-current-name ()
"Get the name of the current workspace."
(safe-persp-name (+workspace-current)))
;;;###autoload
(defun +workspace-list ()
"Return a list of workspace structs (satisifes `+workspace-p')."
;; We don't use `hash-table-values' because it doesn't ensure order in older
;; versions of Emacs
(cdr (cl-loop for persp being the hash-values of *persp-hash*
collect persp)))
;;;###autoload
(defun +workspace-list-names ()
"Return the list of names of open workspaces."
(mapcar #'safe-persp-name (+workspace-list)))
;;;###autoload
(defun +workspace-buffer-list (&optional persp)
"Return a list of buffers in PERSP.
The buffer list is ordered by recency (same as `buffer-list').
PERSP can be a string (name of a workspace) or a workspace (satisfies
`+workspace-p'). If nil or omitted, it defaults to the current workspace."
(let ((persp (or persp (+workspace-current))))
(unless (+workspace-p persp)
(user-error "Not in a valid workspace (%s)" persp))
(persp-buffers persp)))
;;;###autoload
(defun +workspace-orphaned-buffer-list ()
"Return a list of buffers that aren't associated with any perspective."
(cl-remove-if #'persp--buffer-in-persps (buffer-list)))
;;; Actions
;;;###autoload
(defun +workspace-load (name)
"Loads a single workspace (named NAME) into the current session. Can only
retrieve perspectives that were explicitly saved with `+workspace-save'.
Returns t if successful, nil otherwise."
(when (+workspace-exists-p name)
(user-error "A workspace named '%s' already exists." name))
(persp-load-from-file-by-names
(expand-file-name +workspaces-data-file persp-save-dir)
*persp-hash* (list name))
(+workspace-exists-p name))
;;;###autoload
(defun +workspace-save (name)
"Saves a single workspace (NAME) from the current session. Can be loaded again
with `+workspace-load'. NAME can be the string name of a workspace or its
perspective hash table.
Returns t on success, nil otherwise."
(unless (+workspace-exists-p name)
(error "'%s' is an invalid workspace" name))
(let ((fname (expand-file-name +workspaces-data-file persp-save-dir)))
(persp-save-to-file-by-names fname *persp-hash* (list name))
(and (member name (persp-list-persp-names-in-file fname))
t)))
;;;###autoload
(defun +workspace-new (name)
"Create a new workspace named NAME. If one already exists, return nil.
Otherwise return t on success, nil otherwise."
(when (+workspace--protected-p name)
(error "Can't create a new '%s' workspace" name))
(when (+workspace-exists-p name)
(error "A workspace named '%s' already exists" name))
(let ((persp (persp-add-new name))
(+popup--inhibit-transient t))
(save-window-excursion
(let ((ignore-window-parameters t)
(+popup--inhibit-transient t))
(persp-delete-other-windows))
(switch-to-buffer (doom-fallback-buffer))
(setf (persp-window-conf persp)
(funcall persp-window-state-get-function (selected-frame))))
persp))
;;;###autoload
(defun +workspace-rename (name new-name)
"Rename the current workspace named NAME to NEW-NAME. Returns old name on
success, nil otherwise."
(when (+workspace--protected-p name)
(error "Can't rename '%s' workspace" name))
(persp-rename new-name (+workspace-get name)))
;;;###autoload
(defun +workspace-delete (workspace &optional inhibit-kill-p)
"Delete the workspace denoted by WORKSPACE, which can be the name of a perspective
or its hash table. If INHIBIT-KILL-P is non-nil, don't kill this workspace's
buffers."
(unless (stringp workspace)
(setq workspace (persp-name workspace)))
(when (+workspace--protected-p workspace)
(error "Can't delete '%s' workspace" workspace))
(+workspace-get workspace) ; error checking
(persp-kill workspace inhibit-kill-p)
(not (+workspace-exists-p workspace)))
;;;###autoload
(defun +workspace-switch (name &optional auto-create-p)
"Switch to another workspace named NAME (a string).
If AUTO-CREATE-P is non-nil, create the workspace if it doesn't exist, otherwise
throws an error."
(unless (+workspace-exists-p name)
(if auto-create-p
(+workspace-new name)
(error "%s is not an available workspace" name)))
(let ((old-name (+workspace-current-name)))
(setq +workspace--last
(or (and (not (string= old-name persp-nil-name))
old-name)
+workspaces-main))
(persp-frame-switch name)
(equal (+workspace-current-name) name)))
;;
;;; Commands
;;;###autoload
(defalias '+workspace/restore-last-session #'doom/quickload-session)
;;;###autoload
(defun +workspace/load (name)
"Load a workspace and switch to it. If called with C-u, try to reload the
current workspace (by name) from session files."
(interactive
(list
(if current-prefix-arg
(+workspace-current-name)
(completing-read
"Workspace to load: "
(persp-list-persp-names-in-file
(expand-file-name +workspaces-data-file persp-save-dir))))))
(if (not (+workspace-load name))
(+workspace-error (format "Couldn't load workspace %s" name))
(+workspace/switch-to name)
(+workspace/display)))
;;;###autoload
(defun +workspace/save (name)
"Save the current workspace. If called with C-u, autosave the current
workspace."
(interactive
(list
(if current-prefix-arg
(+workspace-current-name)
(completing-read "Workspace to save: " (+workspace-list-names)))))
(if (+workspace-save name)
(+workspace-message (format "'%s' workspace saved" name) 'success)
(+workspace-error (format "Couldn't save workspace %s" name))))
;;;###autoload
(defun +workspace/rename (new-name)
"Rename the current workspace."
(interactive (list (read-from-minibuffer "New workspace name: ")))
(condition-case-unless-debug ex
(let* ((current-name (+workspace-current-name))
(old-name (+workspace-rename current-name new-name)))
(unless old-name
(error "Failed to rename %s" current-name))
(+workspace-message (format "Renamed '%s'->'%s'" old-name new-name) 'success))
('error (+workspace-error ex t))))
;;;###autoload
(defun +workspace/delete (name)
"Delete this workspace. If called with C-u, prompts you for the name of the
workspace to delete."
(interactive
(let ((current-name (+workspace-current-name)))
(list
(if current-prefix-arg
(completing-read (format "Delete workspace (default: %s): " current-name)
(+workspace-list-names)
nil nil nil nil current-name)
current-name))))
(condition-case-unless-debug ex
;; REVIEW refactor me
(let ((workspaces (+workspace-list-names)))
(if (not (member name workspaces))
(+workspace-message (format "'%s' workspace doesn't exist" name) 'warn)
(cond ((delq (selected-frame) (persp-frames-with-persp (get-frame-persp)))
(user-error "Can't close workspace, it's visible in another frame"))
((not (equal (+workspace-current-name) name))
(+workspace-delete name))
((cdr workspaces)
(+workspace-delete name)
(+workspace-switch
(if (+workspace-exists-p +workspace--last)
+workspace--last
(car (+workspace-list-names))))
(unless (doom-buffer-frame-predicate (window-buffer))
(switch-to-buffer (doom-fallback-buffer))))
(t
(+workspace-switch +workspaces-main t)
(unless (string= (car workspaces) +workspaces-main)
(+workspace-delete name))
(doom/kill-all-buffers (doom-buffer-list))))
(+workspace-message (format "Deleted '%s' workspace" name) 'success)))
('error (+workspace-error ex t))))
;;;###autoload
(defun +workspace/kill-session ()
"Delete the current session, all workspaces, windows and their buffers."
(interactive)
(unless (cl-every #'+workspace-delete (+workspace-list-names))
(+workspace-error "Could not clear session"))
(+workspace-switch +workspaces-main t)
(doom/kill-all-buffers (buffer-list)))
;;;###autoload
(defun +workspace/kill-session-and-quit ()
"Kill emacs without saving anything."
(interactive)
(let ((persp-auto-save-opt 0))
(kill-emacs)))
;;;###autoload
(defun +workspace/new (&optional name clone-p)
"Create a new workspace named NAME. If CLONE-P is non-nil, clone the current
workspace, otherwise the new workspace is blank."
(interactive "iP")
(unless name
(setq name (format "#%s" (+workspace--generate-id))))
(condition-case e
(cond ((+workspace-exists-p name)
(error "%s already exists" name))
(clone-p (persp-copy name t))
(t
(+workspace-switch name t)
(+workspace/display)))
((debug error) (+workspace-error (cadr e) t))))
;;;###autoload
(defun +workspace/switch-to (index)
"Switch to a workspace at a given INDEX. A negative number will start from the
end of the workspace list."
(interactive
(list (or current-prefix-arg
(if (featurep! :completion ivy)
(ivy-read "Switch to workspace: "
(+workspace-list-names)
:caller #'+workspace/switch-to
:preselect (+workspace-current-name))
(completing-read "Switch to workspace: " (+workspace-list-names))))))
(when (and (stringp index)
(string-match-p "^[0-9]+$" index))
(setq index (string-to-number index)))
(condition-case-unless-debug ex
(let ((names (+workspace-list-names))
(old-name (+workspace-current-name)))
(cond ((numberp index)
(let ((dest (nth index names)))
(unless dest
(error "No workspace at #%s" (1+ index)))
(+workspace-switch dest)))
((stringp index)
(+workspace-switch index t))
(t
(error "Not a valid index: %s" index)))
(unless (called-interactively-p 'interactive)
(if (equal (+workspace-current-name) old-name)
(+workspace-message (format "Already in %s" old-name) 'warn)
(+workspace/display))))
('error (+workspace-error (cadr ex) t))))
;;;###autoload
(dotimes (i 9)
(defalias (intern (format "+workspace/switch-to-%d" i))
(lambda () (interactive) (+workspace/switch-to i))))
;;;###autoload
(defun +workspace/switch-to-final ()
"Switch to the final workspace in open workspaces."
(interactive)
(+workspace/switch-to (car (last (+workspace-list-names)))))
;;;###autoload
(defun +workspace/other ()
"Switch to the last activated workspace."
(interactive)
(+workspace/switch-to +workspace--last))
;;;###autoload
(defun +workspace/cycle (n)
"Cycle n workspaces to the right (default) or left."
(interactive (list 1))
(let ((current-name (+workspace-current-name)))
(if (equal current-name persp-nil-name)
(+workspace-switch +workspaces-main t)
(condition-case-unless-debug ex
(let* ((persps (+workspace-list-names))
(perspc (length persps))
(index (cl-position current-name persps)))
(when (= perspc 1)
(user-error "No other workspaces"))
(+workspace/switch-to (% (+ index n perspc) perspc))
(unless (called-interactively-p 'interactive)
(+workspace/display)))
('user-error (+workspace-error (cadr ex) t))
('error (+workspace-error ex t))))))
;;;###autoload
(defun +workspace/switch-left () (interactive) (+workspace/cycle -1))
;;;###autoload
(defun +workspace/switch-right () (interactive) (+workspace/cycle +1))
;;;###autoload
(defun +workspace/close-window-or-workspace ()
"Close the selected window. If it's the last window in the workspace, either
close the workspace (as well as its associated frame, if one exists) and move to
the next."
(interactive)
(let ((delete-window-fn (if (featurep 'evil) #'evil-window-delete #'delete-window)))
(if (window-dedicated-p)
(funcall delete-window-fn)
(let ((current-persp-name (+workspace-current-name)))
(cond ((or (+workspace--protected-p current-persp-name)
(cdr (doom-visible-windows)))
(funcall delete-window-fn))
((cdr (+workspace-list-names))
(let ((frame-persp (frame-parameter nil 'workspace)))
(if (string= frame-persp (+workspace-current-name))
(delete-frame)
(+workspace/delete current-persp-name))))
((+workspace-error "Can't delete last workspace" t)))))))
;;
;;; Tabs display in minibuffer
(defun +workspace--tabline (&optional names)
(let ((names (or names (+workspace-list-names)))
(current-name (+workspace-current-name)))
(mapconcat
#'identity
(cl-loop for name in names
for i to (length names)
collect
(propertize (format " [%d] %s " (1+ i) name)
'face (if (equal current-name name)
'+workspace-tab-selected-face
'+workspace-tab-face)))
" ")))
(defun +workspace--message-body (message &optional type)
(concat (+workspace--tabline)
(propertize " | " 'face 'font-lock-comment-face)
(propertize (format "%s" message)
'face (pcase type
('error 'error)
('warn 'warning)
('success 'success)
('info 'font-lock-comment-face)))))
;;;###autoload
(defun +workspace-message (message &optional type)
"Show an 'elegant' message in the echo area next to a listing of workspaces."
(message "%s" (+workspace--message-body message type)))
;;;###autoload
(defun +workspace-error (message &optional noerror)
"Show an 'elegant' error in the echo area next to a listing of workspaces."
(funcall (if noerror #'message #'error)
"%s" (+workspace--message-body message 'error)))
;;;###autoload
(defun +workspace/display ()
"Display a list of workspaces (like tabs) in the echo area."
(interactive)
(let (message-log-max)
(message "%s" (+workspace--tabline))))
;;
;;; Hooks
;;;###autoload
(defun +workspaces-delete-associated-workspace-h (&optional frame)
"Delete workspace associated with current frame.
A workspace gets associated with a frame when a new frame is interactively
created."
(when persp-mode
(unless frame
(setq frame (selected-frame)))
(let ((frame-persp (frame-parameter frame 'workspace)))
(when (string= frame-persp (+workspace-current-name))
(+workspace/delete frame-persp)))))
;;;###autoload
(defun +workspaces-associate-frame-fn (frame &optional _new-frame-p)
"Create a blank, new perspective and associate it with FRAME."
(when persp-mode
(if (not (persp-frame-list-without-daemon))
(+workspace-switch +workspaces-main t)
(with-selected-frame frame
(+workspace-switch (format "#%s" (+workspace--generate-id)) t)
(unless (doom-real-buffer-p (current-buffer))
(switch-to-buffer (doom-fallback-buffer)))
(set-frame-parameter frame 'workspace (+workspace-current-name))
;; ensure every buffer has a buffer-predicate
(persp-set-frame-buffer-predicate frame))
(run-at-time 0.1 nil #'+workspace/display))))
(defvar +workspaces--project-dir nil)
;;;###autoload
(defun +workspaces-set-project-action-fn ()
"A `projectile-switch-project-action' that sets the project directory for
`+workspaces-switch-to-project-h'."
(setq +workspaces--project-dir default-directory))
;;;###autoload
(defun +workspaces-switch-to-project-h (&optional dir)
"Creates a workspace dedicated to a new project. If one already exists, switch
to it. If in the main workspace and it's empty, recycle that workspace, without
renaming it.
Afterwords, runs `+workspaces-switch-project-function'. By default, this prompts
the user to open a file in the new project.
This be hooked to `projectile-after-switch-project-hook'."
(when dir
(setq +workspaces--project-dir dir))
(when (and persp-mode +workspaces--project-dir)
(unwind-protect
(if (and (not (null +workspaces-on-switch-project-behavior))
(or (eq +workspaces-on-switch-project-behavior t)
(+workspace-buffer-list)))
(let* ((persp
(let ((project-name (doom-project-name +workspaces--project-dir)))
(or (+workspace-get project-name t)
(+workspace-new project-name))))
(new-name (persp-name persp)))
(+workspace-switch new-name)
(with-current-buffer (doom-fallback-buffer)
(setq default-directory +workspaces--project-dir))
(unless current-prefix-arg
(funcall +workspaces-switch-project-function +workspaces--project-dir))
(+workspace-message
(format "Switched to '%s' in new workspace" new-name)
'success))
(with-current-buffer (doom-fallback-buffer)
(setq default-directory +workspaces--project-dir)
(message "Switched to '%s'" (doom-project-name +workspaces--project-dir)))
(with-demoted-errors "Workspace error: %s"
(+workspace-rename (+workspace-current-name) (doom-project-name +workspaces--project-dir)))
(unless current-prefix-arg
(funcall +workspaces-switch-project-function +workspaces--project-dir)))
(setq +workspaces--project-dir nil))))
;;
;;; Advice
;;;###autoload
(defun +workspaces-autosave-real-buffers-a (orig-fn &rest args)
"Don't autosave if no real buffers are open."
(when (doom-real-buffer-list)
(apply orig-fn args))
t)

View File

@@ -0,0 +1,222 @@
;;; ui/workspaces/config.el -*- lexical-binding: t; -*-
;; `persp-mode' gives me workspaces, a workspace-restricted `buffer-list', and
;; file-based session persistence. I used workgroups2 before this, but abandoned
;; it because it was unstable and slow; `persp-mode' is neither (and still
;; maintained).
;;
;; NOTE persp-mode requires `workgroups' for file persistence in Emacs 24.4.
(defvar +workspaces-main "main"
"The name of the primary and initial workspace, which cannot be deleted.")
(defvar +workspaces-switch-project-function #'doom-project-find-file
"The function to run after `projectile-switch-project' or
`counsel-projectile-switch-project'. This function must take one argument: the
new project directory.")
(defvar +workspaces-on-switch-project-behavior 'non-empty
"Controls the behavior of workspaces when switching to a new project.
Can be one of the following:
t Always create a new workspace for the project
'non-empty Only create a new workspace if the current one already has buffers
associated with it.
nil Never create a new workspace on project switch.")
;; FIXME actually use this for wconf bookmark system
(defvar +workspaces-data-file "_workspaces"
"The basename of the file to store single workspace perspectives. Will be
stored in `persp-save-dir'.")
(defvar +workspace--old-uniquify-style nil)
;;
;; Packages
(use-package! persp-mode
:commands persp-switch-to-buffer
:init
(add-hook! 'doom-init-modules-hook
(defun +workspaces-init-h ()
(unless noninteractive
;; Remove default buffer predicate so persp-mode can put in its own
(delq! 'buffer-predicate default-frame-alist 'assq)
(require 'persp-mode)
(if (daemonp)
(add-hook 'after-make-frame-functions #'persp-mode-start-and-remove-from-make-frame-hook)
(persp-mode +1)))))
:config
(setq persp-autokill-buffer-on-remove 'kill-weak
persp-nil-hidden t
persp-auto-save-fname "autosave"
persp-save-dir (concat doom-etc-dir "workspaces/")
persp-set-last-persp-for-new-frames t
persp-switch-to-added-buffer nil
persp-remove-buffers-from-nil-persp-behaviour nil
persp-auto-resume-time -1 ; Don't auto-load on startup
persp-auto-save-opt (if noninteractive 0 1)) ; auto-save on kill
(advice-add #'persp-asave-on-exit :around #'+workspaces-autosave-real-buffers-a)
(add-hook! '(persp-mode-hook persp-after-load-state-functions)
(defun +workspaces-ensure-main-workspace-h (&rest _)
"Ensure the main workspace exists and the nil workspace is never active."
(when persp-mode
(let (persp-before-switch-functions)
;; The default perspective persp-mode creates (`persp-nil-name') is
;; special and doesn't represent a real persp object, so buffers can't
;; really be assigned to it, among other quirks. We create a *real* main
;; workspace to fill this role.
(unless (persp-get-by-name +workspaces-main)
(persp-add-new +workspaces-main))
;; Switch to it if we're in the nil perspective
(dolist (frame (frame-list))
(when (string= (safe-persp-name (get-current-persp frame)) persp-nil-name)
(persp-frame-switch +workspaces-main frame)
;; Fix #319: the warnings buffer gets swallowed by creating
;; `+workspaces-main', so we display it manually, if it exists.
(when-let (warnings (get-buffer "*Warnings*"))
(save-excursion
(display-buffer-in-side-window
warnings '((window-height . shrink-window-if-larger-than-buffer)))))))))))
(add-hook! 'persp-mode-hook
(defun +workspaces-init-persp-mode-h ()
(cond (persp-mode
;; `uniquify' breaks persp-mode. It renames old buffers, which causes
;; errors when switching between perspective (their buffers are
;; serialized by name and persp-mode expects them to have the same
;; name when restored).
(when uniquify-buffer-name-style
(setq +workspace--old-uniquify-style uniquify-buffer-name-style))
(setq uniquify-buffer-name-style nil)
;; Ensure `persp-kill-buffer-query-function' is last
(remove-hook 'kill-buffer-query-functions #'persp-kill-buffer-query-function)
(add-hook 'kill-buffer-query-functions #'persp-kill-buffer-query-function t)
;; Restrict buffer list to workspace
(advice-add #'doom-buffer-list :override #'+workspace-buffer-list))
(t
(when +workspace--old-uniquify-style
(setq uniquify-buffer-name-style +workspace--old-uniquify-style))
(advice-remove #'doom-buffer-list #'+workspace-buffer-list)))))
;; We don't rely on the built-in mechanism for auto-registering a buffer to
;; the current workspace; some buffers slip through the cracks. Instead, we
;; add buffers when they are switched to.
(setq persp-add-buffer-on-find-file nil
persp-add-buffer-on-after-change-major-mode nil)
(add-hook! '(doom-switch-buffer-hook server-visit-hook)
(defun +workspaces-add-current-buffer-h ()
"Add current buffer to focused perspective."
(and persp-mode
(not (persp-buffer-filtered-out-p
(current-buffer)
persp-add-buffer-on-after-change-major-mode-filter-functions))
(persp-add-buffer (current-buffer) (get-current-persp) nil nil))))
(add-hook 'persp-add-buffer-on-after-change-major-mode-filter-functions
#'doom-unreal-buffer-p)
(defadvice! +workspaces--evil-alternate-buffer-a (&optional window)
"Make `evil-alternate-buffer' ignore buffers outside the current workspace."
:override #'evil-alternate-buffer
(let* ((prev-buffers
(if persp-mode
(cl-remove-if-not #'persp-contain-buffer-p (window-prev-buffers)
:key #'car)
(window-prev-buffers)))
(head (car prev-buffers)))
(if (eq (car head) (window-buffer window))
(cadr prev-buffers)
head)))
;; Delete the current workspace if closing the last open window
(define-key! persp-mode-map
[remap delete-window] #'+workspace/close-window-or-workspace
[remap evil-window-delete] #'+workspace/close-window-or-workspace)
;; per-frame workspaces
(setq persp-init-frame-behaviour t
persp-init-new-frame-behaviour-override nil
persp-interactive-init-frame-behaviour-override #'+workspaces-associate-frame-fn
persp-emacsclient-init-frame-behaviour-override #'+workspaces-associate-frame-fn)
(add-hook 'delete-frame-functions #'+workspaces-delete-associated-workspace-h)
;; per-project workspaces, but reuse current workspace if empty
(setq projectile-switch-project-action #'+workspaces-set-project-action-fn
counsel-projectile-switch-project-action
'(1 ("o" +workspaces-switch-to-project-h "open project in new workspace")
("O" counsel-projectile-switch-project-action "jump to a project buffer or file")
("f" counsel-projectile-switch-project-action-find-file "jump to a project file")
("d" counsel-projectile-switch-project-action-find-dir "jump to a project directory")
("b" counsel-projectile-switch-project-action-switch-to-buffer "jump to a project buffer")
("m" counsel-projectile-switch-project-action-find-file-manually "find file manually from project root")
("w" counsel-projectile-switch-project-action-save-all-buffers "save all project buffers")
("k" counsel-projectile-switch-project-action-kill-buffers "kill all project buffers")
("r" counsel-projectile-switch-project-action-remove-known-project "remove project from known projects")
("c" counsel-projectile-switch-project-action-compile "run project compilation command")
("C" counsel-projectile-switch-project-action-configure "run project configure command")
("e" counsel-projectile-switch-project-action-edit-dir-locals "edit project dir-locals")
("v" counsel-projectile-switch-project-action-vc "open project in vc-dir / magit / monky")
("s" (lambda (project)
(let ((projectile-switch-project-action
(lambda () (call-interactively #'+ivy/project-search))))
(counsel-projectile-switch-project-by-name project))) "search project")
("xs" counsel-projectile-switch-project-action-run-shell "invoke shell from project root")
("xe" counsel-projectile-switch-project-action-run-eshell "invoke eshell from project root")
("xt" counsel-projectile-switch-project-action-run-term "invoke term from project root")
("X" counsel-projectile-switch-project-action-org-capture "org-capture into project")))
(add-hook 'projectile-after-switch-project-hook #'+workspaces-switch-to-project-h)
;; Fix #1973: visual selection surviving workspace changes
(add-hook 'persp-before-deactivate-functions #'deactivate-mark)
;; Fix #1017: stop session persistence from restoring a broken posframe
(after! posframe
(add-hook! 'persp-after-load-state-functions
(defun +workspaces-delete-all-posframes-h (&rest _)
(posframe-delete-all))))
;; Fix #1525: Ignore dead buffers in PERSP's buffer list
(defun +workspaces-dead-buffer-p (buf)
(not (buffer-live-p buf)))
(add-hook 'persp-filter-save-buffers-functions #'+workspaces-dead-buffer-p)
;;
;; eshell
(persp-def-buffer-save/load
:mode 'eshell-mode :tag-symbol 'def-eshell-buffer
:save-vars '(major-mode default-directory))
;; compile
(persp-def-buffer-save/load
:mode 'compilation-mode :tag-symbol 'def-compilation-buffer
:save-vars
'(major-mode default-directory compilation-directory compilation-environment compilation-arguments))
;; Restore indirect buffers
(defvar +workspaces--indirect-buffers-to-restore nil)
(persp-def-buffer-save/load
:tag-symbol 'def-indirect-buffer
:predicate #'buffer-base-buffer
:save-function (lambda (buf tag vars)
(list tag (buffer-name buf) vars
(buffer-name (buffer-base-buffer buf))))
:load-function (lambda (savelist &rest _rest)
(cl-destructuring-bind (buf-name _vars base-buf-name &rest _)
(cdr savelist)
(push (cons buf-name base-buf-name)
+workspaces--indirect-buffers-to-restore)
nil)))
(add-hook! 'persp-after-load-state-functions
(defun +workspaces-reload-indirect-buffers-h (&rest _)
(dolist (ibc +workspaces--indirect-buffers-to-restore)
(cl-destructuring-bind (buffer-name . base-buffer-name) ibc
(when (buffer-live-p (get-buffer base-buffer-name))
(when (get-buffer buffer-name)
(setq buffer-name (generate-new-buffer-name buffer-name)))
(make-indirect-buffer bb buffer-name t))))
(setq +workspaces--indirect-buffers-to-restore nil))))

View File

@@ -0,0 +1,5 @@
;; -*- no-byte-compile: t; -*-
;;; ui/workspaces/packages.el
(package! persp-mode)

View File

@@ -0,0 +1,124 @@
;; -*- no-byte-compile: t; -*-
;;; ui/workspaces/test/test-workspaces.el
(describe "ui/workspaces"
:var (persp-auto-resume-time
persp-auto-save-opt
persp-switch-to-added-buffer
persp-autokill-persp-when-removed-last-buffer
persp-autokill-buffer-on-remove
in1 in2 out1 out2
persp1 persp1-name persp2 persp2-name
wconf)
(require! :ui workspaces)
(require 'persp-mode)
(before-all
(delete-other-windows))
(before-each
(switch-to-buffer "*scratch*")
(setq wconf (current-window-configuration)
persp-auto-resume-time -1
persp-auto-save-opt 0
persp-switch-to-added-buffer nil
persp-autokill-persp-when-removed-last-buffer nil
persp-autokill-buffer-on-remove nil
in1 (get-buffer-create "in1")
in2 (get-buffer-create "in2")
out1 (get-buffer-create "out1")
out2 (get-buffer-create "out2"))
(doom-set-buffer-real in1 t)
(doom-set-buffer-real out1 t)
(let (noninteractive)
(persp-mode +1)
(let (persp-before-switch-functions persp-activated-functions)
(setq persp1-name +workspaces-main
persp1 (persp-add-new persp1-name)
persp2-name "test"
persp2 (persp-add-new persp2-name))
(persp-switch persp1-name)
(persp-add-buffer (list in1 in2) persp1))))
(after-each
(let (kill-buffer-query-functions kill-buffer-hook)
(let (noninteractive ignore-window-parameters)
(dolist (persp (persp-names))
(ignore-errors (persp-kill persp)))
(persp-mode -1))
(set-window-configuration wconf)
(mapc #'kill-buffer (list in1 in2 out1 out2))))
;;
(describe "switch"
(it "throws an error when switching to a non-existent workspace"
(expect (+workspace-switch "non-existent") :to-throw))
(it "switches to a valid workspace"
(+workspace-switch persp2-name)
(expect (+workspace-current-name) :to-equal persp2-name)))
(describe "current"
(it "returns the current workspace persp"
(expect (+workspace-p (+workspace-current)))
(expect (+workspace-current) :to-equal (get-current-persp)))
(it "returns the current workspace's name"
(expect (+workspace-current-name) :to-equal persp1-name)
(persp-switch (persp-name persp2))
(expect (+workspace-current-name) :to-equal persp2-name)))
(describe "exists-p"
(it "returns t for valid workspaces"
(expect (+workspace-exists-p persp1-name)))
(it "returns t for non-current (but valid) workspaces"
(expect (+workspace-exists-p persp2-name)))
(it "returns nil for non-existent workspaces"
(expect (+workspace-exists-p "non-existent") :to-be nil)))
(describe "buffer membership"
(it "returns t for buffers in current workspace"
(expect (+workspace-contains-buffer-p in1)))
(it "returns nil for buffers outside of current workspace"
(expect (+workspace-contains-buffer-p out1) :to-be nil))
(xit "returns a list of orphaned buffers"
(expect (+workspace-orphaned-buffer-list) :to-contain out2)))
(describe "list"
(it "returns a list of names"
(expect (+workspace-list-names)
:to-have-same-items-as (list persp1-name persp2-name)))
(it "returns a list of perspective structs"
(expect (+workspace-list)
:to-have-same-items-as (list persp1 persp2))))
(describe "CRUD"
(it "creates new workspaces"
(+workspace-new "X")
(expect (+workspace-list-names) :to-contain "X"))
(it "renames an existing workspace"
(+workspace-rename persp2-name "X")
(expect (persp-name persp2) :to-equal "X")
(expect (+workspace-list-names)
:to-have-same-items-as (list persp1-name "X")))
(it "deletes a live workspace"
(+workspace-delete persp2-name)
(expect (+workspace-list-names) :not :to-contain persp2-name)))
(describe "command"
(describe "close-window-or-workspace"
(before-each
(+workspace-switch persp2-name)
(split-window)
(expect (length (doom-visible-windows)) :to-be 2))
(it "kills window if more than one window"
(quiet! (+workspace/close-window-or-workspace))
(expect (length (doom-visible-windows)) :to-be 1))
(it "kills workspace on last window"
(quiet! (+workspace/close-window-or-workspace)
(+workspace/close-window-or-workspace))
(expect (+workspace-current-name) :to-equal persp1-name)))
(describe "rename"
(it "renames the current workspace"
(quiet! (+workspace/rename "X"))
(expect (+workspace-current-name) :to-equal "X")))))