1 ;;; darkroom.el --- Remove visual distractions and focus on writing -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2014 Free Software Foundation, Inc.
5 ;; Author: João Távora <joaotavora@gmail.com>
6 ;; Maintainer: João Távora <joaotavora@gmail.com>
7 ;; Keywords: convenience, emulations
8 ;; Package-Requires: ((cl-lib "0.5"))
11 ;; This program is free software; you can redistribute it and/or modify
12 ;; it under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation, either version 3 of the License, or
14 ;; (at your option) any later version.
16 ;; This program is distributed in the hope that it will be useful,
17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 ;; GNU General Public License for more details.
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
26 ;; The main entrypoints to this extension are two minor modes:
29 ;; M-x darkroom-tentative-mode
31 ;; `darkroom-mode' makes visual distractions disappear: the
32 ;; mode-line is temporarily elided, text is enlarged and margins are
33 ;; adjusted so that it's centered on the window.
35 ;; `darkroom-tentative-mode' is similar, but it doesn't immediately
36 ;; turn-on `darkroom-mode', unless the current buffer lives in the
37 ;; sole window of the Emacs frame (i.e. all other windows are
38 ;; deleted). Whenever the frame is split to display more windows and
39 ;; more buffers, the buffer exits `darkroom-mode'. Whenever they are
40 ;; deleted, the buffer re-enters `darkroom-mode'.
42 ;; Personally, I always use `darkroom-tentative-mode'.
44 ;; See also the customization options `darkroom-margins' and
45 ;; `darkroom-fringes-outside-margins', which affect both modes.
51 (defgroup darkroom nil
52 "Remove visual distractions and focus on writing"
56 (defcustom darkroom-margins 'darkroom-guess-margins
57 "Margins to use in `darkroom-mode'.
61 - a floating point value betweeen 0 and 1, specifies percentage of
62 window width in columns to use as a margin.
64 - a cons cell (LEFT RIGHT) specifying the left and right margins
67 - a function of a single argument, a window, that returns a cons
68 cell interpreted like the previous option. An example is
69 `darkroom-guess-margins', which see. Beware that this function
70 is called very often, so if it does some non-trivial processing
71 on the buffer's text, consider caching that value.
73 Value is effective when `darkroom-mode' is toggled."
75 (cons integer integer)
76 (function-item darkroom-guess-margins :doc "Guess margins")
77 (function darkroom-guess-margins))
80 (defcustom darkroom-text-scale-increase 2
81 "Steps to increase text size when in `darkroom-mode'.
82 Value is passed to `text-scale-increase'."
86 (defcustom darkroom-fringes-outside-margins t
87 "If non-nil use fringes outside margins for `darkroom-mode'"
91 (defcustom darkroom-margin-increment 0.05
92 "Increment to add used in `darkroom-increase-margins'."
96 (defcustom darkroom-margins-if-failed-guess 0.15
97 "Margins when `darkroom-guess-margins' fails.
98 If `darkroom-guess-margins' failed to figure out margins to
99 center the text, use this percentage of window width for the
104 (defcustom darkroom-verbose nil
105 "If non-nil, be verbose about darkroom operations."
109 (defvar darkroom--guess-margins-statistics-cache nil
110 "Cache used by `darkroom-guess-margins'.")
112 (defun darkroom--window-width (&optional window)
113 "Calculate width of WINDOW in columns, considering text scaling.
114 WINDOW defaults to the currently selected window. The function
115 assumes the buffer to be filled with at least one character of an
116 arbitrary, but fixed width. Narrowing is taken in consideration.
117 The return value is a cons (COLS . SCALED-CHAR-WIDTH) where COLS
118 is the desired width in columns and SCALED-CHAR-WIDTH is the
119 width in pixels of a single character."
120 (when (= (point-min) (point-max))
121 (error "Cannot calculate the width of a single character"))
122 (let* ((window (or window (selected-window)))
123 (scaled-char-width (car (window-text-pixel-size
125 (point-min) (1+ (point-min)))))
126 (char-width (frame-char-width))
127 (margins (window-margins window)))
129 (+ (window-width window 'pixelwise)
130 (* char-width (or (car margins) 0))
131 (* char-width (or (cdr margins) 0)))
135 (defun darkroom-guess-margins (window)
136 "Guess suitable margins for `darkroom-margins'.
137 If in suitable conditions, collect some statistics about the
138 buffer's line lengths, and apply a heuristic to figure out how
139 wide to set the margins, comparing it to WINDOW's width in
140 columns. If the buffer's paragraphs are mostly filled to
141 `fill-column', margins should center it on the window, otherwise,
142 the margins specified in `darkroom-margins-if-failed-guess'.
144 In any of these conditions,`darkroom-margins-if-failed-guess' is
147 * if `visual-line-mode' is on;
148 * if `variable-pitch-mode' is on;
149 * if the buffer is empty.
151 For testing purposes, WINDOW can also be an integer number which
152 is a width in columns, in which case it will be used instead of a
154 (if (or visual-line-mode
155 (and buffer-face-mode
156 (eq 'variable-pitch buffer-face-mode-face))
157 (= (point-min) (point-max)))
158 darkroom-margins-if-failed-guess
159 (let* ((window-width-info (if (integerp window)
161 (darkroom--window-width window)))
162 (window-width (car window-width-info))
163 (scaled-char-width (cdr window-width-info))
165 (or darkroom--guess-margins-statistics-cache
167 (make-local-variable 'darkroom--guess-margins-statistics-cache)
170 (goto-char (point-min))
171 (cl-loop for start = (point)
172 while (search-forward "\n"
175 for width = (truncate
177 (window-text-pixel-size
183 (n4 (max 1 (/ (length line-widths) 4))))
184 (/ (apply '+ (cl-subseq (sort line-widths '>) 0 n4)) n4))))))
188 (message "Long lines detected. Consider turning on `visual-line-mode'")
189 darkroom-margins-if-failed-guess)
190 ((> top-quartile-avg (* 0.9 fill-column))
191 ;; calculate margins so that `fill-column' + 1 colums are
192 ;; centered on the window.
194 (let ((margin (truncate (* (- window-width (1+ fill-column))
195 (/ (float scaled-char-width)
199 (message "Choosing %s-wide margins based on fill-column %s"
201 (cons margin margin)))
203 darkroom-margins-if-failed-guess)))))
205 (defun darkroom--compute-margins (window)
206 "From `darkroom-margins', computes desired margins for WINDOW."
207 (let ((darkroom-margins
208 (if (functionp darkroom-margins)
209 (funcall darkroom-margins window)
211 (cond ((consp darkroom-margins)
213 ((and (floatp darkroom-margins)
214 (< darkroom-margins 1))
215 (let ((delta (darkroom--float-to-columns darkroom-margins)))
218 (error "Illegal value in `darkroom-margins'")))))
220 (defun darkroom--float-to-columns (f)
221 (ceiling (* (let ((edges (window-edges)))
222 (- (nth 2 edges) (nth 0 edges)))
225 (defvar darkroom--margin-factor 1
226 "Buffer local factor affecting `darkroom--set-margins'")
228 (defun darkroom--set-margins ()
229 "Set darkroom margins for currently selected window"
230 (let* ((window-configuration-change-hook nil)
231 (window (selected-window))
232 (margins (darkroom--compute-margins window)))
233 ;; See description of
234 ;; `fringes-outside-margins' for the reason
235 ;; for this apparent noop
236 (set-window-buffer window (current-buffer))
237 (set-window-margins window
239 (* darkroom--margin-factor
242 (* darkroom--margin-factor
245 (defun darkroom--reset-margins ()
246 "Reset darkroom margins for currently selected window."
247 (set-window-margins (selected-window) 0 0))
249 (defun darkroom-increase-margins (increment)
250 "Increase darkroom margins by INCREMENT."
251 (interactive (list darkroom-margin-increment))
252 (set (make-local-variable 'darkroom--margin-factor)
253 (* darkroom--margin-factor (+ 1 increment)))
255 (with-selected-window w
256 (darkroom--set-margins)))
257 (get-buffer-window-list (current-buffer))))
259 (defun darkroom-decrease-margins (decrement)
260 "Decrease darkroom margins by DECREMENT."
261 (interactive (list darkroom-margin-increment))
262 (darkroom-increase-margins (- decrement)))
264 (defvar darkroom-mode-map
265 (let ((map (make-sparse-keymap)))
266 (define-key map (kbd "C-M-+") 'darkroom-increase-margins)
267 (define-key map (kbd "C-M--") 'darkroom-decrease-margins)
270 (defconst darkroom--saved-variables
273 fringes-outside-margins)
274 "Variables saved in `darkroom--saved-state'")
276 (defvar darkroom--saved-state nil
277 "Saved state before `darkroom-mode' is turned on.
278 Alist of (VARIABLE . BEFORE-VALUE)")
280 ;; (defvar darkroom--saved-text-scale-mode-amount nil
281 ;; "Text scale before `darkroom-mode' is turned on.")
283 (defun darkroom--enter (&optional just-margins)
284 "Save current state and enter darkroom for the current buffer.
285 With optional JUST-MARGINS, just set the margins."
287 (setq darkroom--saved-state
288 (mapcar #'(lambda (sym)
289 (cons sym (buffer-local-value sym (current-buffer))))
290 darkroom--saved-variables))
291 (setq mode-line-format nil
292 header-line-format nil
293 fringes-outside-margins darkroom-fringes-outside-margins)
294 (text-scale-increase darkroom-text-scale-increase))
296 (with-selected-window w
297 (darkroom--set-margins)))
298 (get-buffer-window-list (current-buffer))))
300 (defun darkroom--leave ()
301 "Undo the effects of `darkroom--enter'."
302 (mapc #'(lambda (pair)
303 (set (make-local-variable (car pair)) (cdr pair)))
304 darkroom--saved-state)
305 (setq darkroom--saved-state nil)
306 (text-scale-decrease darkroom-text-scale-increase)
308 (with-selected-window w
309 (darkroom--reset-margins)))
310 (get-buffer-window-list (current-buffer))))
312 (defun darkroom--enter-or-leave ()
313 "Enter or leave darkroom according to window configuration."
314 (cond ((= (count-windows) 1)
315 (darkroom--enter darkroom--saved-state))
316 (darkroom--saved-state
319 ;; for clarity, don't do anything
322 (declare-function darkroom-tentative-mode "darkroom" t)
325 (define-minor-mode darkroom-mode
326 "Remove visual distractions and focus on writing. When this
327 mode is active, everything but the buffer's text is elided from
328 view. The buffer margins are set so that text is centered on
329 screen. Text size is increased (display engine allowing) by
330 `darkroom-text-scale-increase'." nil nil nil
331 (when darkroom-tentative-mode
334 (concat "Turning off `darkroom-tentative-mode' first. "
335 "It doesn't go with `darkroom-mode'.")
336 (let ((darkroom-mode nil))
337 (darkroom-tentative-mode -1))))
340 (add-hook 'window-configuration-change-hook 'darkroom--set-margins
344 (remove-hook 'window-configuration-change-hook 'darkroom--set-margins
348 (define-minor-mode darkroom-tentative-mode
349 "Enters `darkroom-mode' when all other windows are deleted."
350 nil " Room" darkroom-mode-map
351 ;; always begin by removing the hook
353 (remove-hook 'window-configuration-change-hook
354 'darkroom--enter-or-leave 'local)
358 (concat "Turning off `darkroom-mode' first. "
359 "It doesn't go with `darkroom-tentative-mode'.")
360 (let ((darkroom-tentative-mode nil))
361 (darkroom-mode -1))))
362 ;; turn darkroom on or off according to window state
364 (cond (darkroom-tentative-mode
365 ;; re-add the hook when we are turning ourselves on
367 (add-hook 'window-configuration-change-hook
368 'darkroom--enter-or-leave 'append 'local)
369 ;; call this right away if we're supposed to turn darkroom on
372 (darkroom--enter-or-leave))
378 ;;; darkroom.el ends here