1 ;;; darkroom.el --- Remove visual distractions and focus on writing -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2014 João Távora
5 ;; Author: João Távora <joaotavora@gmail.com>
6 ;; Keywords: convenience, emulations
7 ;; Package-Requires: ((cl-lib "0.5"))
10 ;; This program is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
15 ;; This program is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
25 ;; The main entrypoints to this extension are two minor modes:
28 ;; M-x darkroom-tentative-mode
30 ;; `darkroom-mode' makes visual distractions disappear: the
31 ;; mode-line is temporarily elided, text is enlarged and margins are
32 ;; adjusted so that it's centered on the window.
34 ;; `darkroom-tentative-mode' is similar, but it doesn't immediately
35 ;; turn-on `darkroom-mode', unless the current buffer lives in the
36 ;; sole window of the Emacs frame (i.e. all other windows are
37 ;; deleted). Whenever the frame is split to display more windows and
38 ;; more buffers, the buffer exits `darkroom-mode'. Whenever they are
39 ;; deleted, the buffer re-enters `darkroom-mode'.
41 ;; Personally, I always use `darkroom-tentative-mode'.
43 ;; See also the customization options `darkroom-margins' and
44 ;; `darkroom-fringes-outside-margins', which affect both modes.
50 (defgroup darkroom nil
51 "Remove visual distractions and focus on writing"
55 (defcustom darkroom-margins 'darkroom-guess-margins
56 "Margins to use in `darkroom-mode'.
60 - a floating point value betweeen 0 and 1, specifies percentage of
61 window width in columns to use as a margin.
63 - a cons cell (LEFT RIGHT) specifying the left and right margins
66 - a function of a single argument, a window, that returns a cons
67 cell interpreted like the previous option. An example is
68 `darkroom-guess-margins', which see. Beware that this function
69 is called very often, so if it does some non-trivial processing
70 on the buffer's text, consider caching that value.
72 Value is effective when `darkroom-mode' is toggled."
74 (cons integer integer)
75 (function-item darkroom-guess-margins :doc "Guess margins")
76 (function darkroom-guess-margins))
79 (defcustom darkroom-text-scale-increase 2
80 "Steps to increase text size when in `darkroom-mode'.
81 Value is passed to `text-scale-increase'."
85 (defcustom darkroom-fringes-outside-margins t
86 "If non-nil use fringes outside margins for `darkroom-mode'"
90 (defcustom darkroom-margin-increment 0.05
91 "Increment to add used in `darkroom-increase-margins'."
95 (defcustom darkroom-margins-if-failed-guess 0.15
96 "Margins when `darkroom-guess-margins' fails.
97 If `darkroom-guess-margins' failed to figure out margins to
98 center the text, use this percentage of window width for the
103 (defcustom darkroom-verbose nil
104 "If non-nil, be verbose about darkroom operations."
108 (defvar darkroom--guess-margins-statistics-cache nil
109 "Cache used by `darkroom-guess-margins'.")
111 (defun darkroom--window-width (&optional window)
112 "Calculate width of WINDOW in columns, considering text scaling.
113 WINDOW defaults to the currently selected window. The function
114 assumes the buffer to be filled with at least one character of an
115 arbitrary, but fixed width. Narrowing is taken in consideration.
116 The return value is a cons (COLS . SCALED-CHAR-WIDTH) where COLS
117 is the desired width in columns and SCALED-CHAR-WIDTH is the
118 width in pixels of a single character."
119 (when (= (point-min) (point-max))
120 (error "Cannot calculate the width of a single character"))
121 (let* ((window (or window (selected-window)))
122 (scaled-char-width (car (window-text-pixel-size
124 (point-min) (1+ (point-min)))))
125 (char-width (frame-char-width))
126 (margins (window-margins window)))
128 (+ (window-width window 'pixelwise)
129 (* char-width (or (car margins) 0))
130 (* char-width (or (cdr margins) 0)))
134 (defun darkroom-guess-margins (window)
135 "Guess suitable margins for `darkroom-margins'.
136 If in suitable conditions, collect some statistics about the
137 buffer's line lengths, and apply a heuristic to figure out how
138 wide to set the margins, comparing it to WINDOW's width in
139 columns. If the buffer's paragraphs are mostly filled to
140 `fill-column', margins should center it on the window, otherwise,
141 the margins specified in `darkroom-margins-if-failed-guess'.
143 In any of these conditions,`darkroom-margins-if-failed-guess' is
146 * if `visual-line-mode' is on;
147 * if `variable-pitch-mode' is on;
148 * if the buffer is empty.
150 For testing purposes, WINDOW can also be an integer number which
151 is a width in columns, in which case it will be used instead of a
153 (if (or visual-line-mode
154 (and buffer-face-mode
155 (eq 'variable-pitch buffer-face-mode-face))
156 (= (point-min) (point-max)))
157 darkroom-margins-if-failed-guess
158 (let* ((window-width-info (if (integerp window)
160 (darkroom--window-width window)))
161 (window-width (car window-width-info))
162 (scaled-char-width (cdr window-width-info))
164 (or darkroom--guess-margins-statistics-cache
166 (make-local-variable 'darkroom--guess-margins-statistics-cache)
169 (goto-char (point-min))
170 (cl-loop for start = (point)
171 while (search-forward "\n"
174 for width = (truncate
176 (window-text-pixel-size
182 (n4 (max 1 (/ (length line-widths) 4))))
183 (/ (apply '+ (cl-subseq (sort line-widths '>) 0 n4)) n4))))))
187 (message "Long lines detected. Consider turning on `visual-line-mode'")
188 darkroom-margins-if-failed-guess)
189 ((> top-quartile-avg (* 0.9 fill-column))
190 ;; calculate margins so that `fill-column' + 1 colums are
191 ;; centered on the window.
193 (let ((margin (truncate (* (- window-width (1+ fill-column))
194 (/ (float scaled-char-width)
198 (message "Choosing %s-wide margins based on fill-column %s"
200 (cons margin margin)))
202 darkroom-margins-if-failed-guess)))))
204 (defun darkroom--compute-margins (window)
205 "From `darkroom-margins', computes desired margins for WINDOW."
206 (let ((darkroom-margins
207 (if (functionp darkroom-margins)
208 (funcall darkroom-margins window)
210 (cond ((consp darkroom-margins)
212 ((and (floatp darkroom-margins)
213 (< darkroom-margins 1))
214 (let ((delta (darkroom--float-to-columns darkroom-margins)))
217 (error "Illegal value in `darkroom-margins'")))))
219 (defun darkroom--float-to-columns (f)
220 (ceiling (* (let ((edges (window-edges)))
221 (- (nth 2 edges) (nth 0 edges)))
224 (defvar darkroom--margin-factor 1
225 "Buffer local factor affecting `darkroom--set-margins'")
227 (defun darkroom--set-margins ()
228 "Set darkroom margins for currently selected window"
229 (let* ((window-configuration-change-hook nil)
230 (window (selected-window))
231 (margins (darkroom--compute-margins window)))
232 ;; See description of
233 ;; `fringes-outside-margins' for the reason
234 ;; for this apparent noop
235 (set-window-buffer window (current-buffer))
236 (set-window-margins window
238 (* darkroom--margin-factor
241 (* darkroom--margin-factor
244 (defun darkroom--reset-margins ()
245 "Reset darkroom margins for currently selected window."
246 (set-window-margins (selected-window) 0 0))
248 (defun darkroom-increase-margins (increment)
249 "Increase darkroom margins by INCREMENT."
250 (interactive (list darkroom-margin-increment))
251 (set (make-local-variable 'darkroom--margin-factor)
252 (* darkroom--margin-factor (+ 1 increment)))
254 (with-selected-window w
255 (darkroom--set-margins)))
256 (get-buffer-window-list (current-buffer))))
258 (defun darkroom-decrease-margins (decrement)
259 "Decrease darkroom margins by DECREMENT."
260 (interactive (list darkroom-margin-increment))
261 (darkroom-increase-margins (- decrement)))
263 (defvar darkroom-mode-map
264 (let ((map (make-sparse-keymap)))
265 (define-key map (kbd "C-M-+") 'darkroom-increase-margins)
266 (define-key map (kbd "C-M--") 'darkroom-decrease-margins)
269 (defconst darkroom--saved-variables
272 fringes-outside-margins)
273 "Variables saved in `darkroom--saved-state'")
275 (defvar darkroom--saved-state nil
276 "Saved state before `darkroom-mode' is turned on.
277 Alist of (VARIABLE . BEFORE-VALUE)")
279 ;; (defvar darkroom--saved-text-scale-mode-amount nil
280 ;; "Text scale before `darkroom-mode' is turned on.")
282 (defun darkroom--enter (&optional just-margins)
283 "Save current state and enter darkroom for the current buffer.
284 With optional JUST-MARGINS, just set the margins."
286 (setq darkroom--saved-state
287 (mapcar #'(lambda (sym)
288 (cons sym (buffer-local-value sym (current-buffer))))
289 darkroom--saved-variables))
290 (setq mode-line-format nil
291 header-line-format nil
292 fringes-outside-margins darkroom-fringes-outside-margins)
293 (text-scale-increase darkroom-text-scale-increase))
295 (with-selected-window w
296 (darkroom--set-margins)))
297 (get-buffer-window-list (current-buffer))))
299 (defun darkroom--leave ()
300 "Undo the effects of `darkroom--enter'."
301 (mapc #'(lambda (pair)
302 (set (make-local-variable (car pair)) (cdr pair)))
303 darkroom--saved-state)
304 (setq darkroom--saved-state nil)
305 (text-scale-decrease darkroom-text-scale-increase)
307 (with-selected-window w
308 (darkroom--reset-margins)))
309 (get-buffer-window-list (current-buffer))))
311 (defun darkroom--enter-or-leave ()
312 "Enter or leave darkroom according to window configuration."
313 (cond ((= (count-windows) 1)
314 (darkroom--enter darkroom--saved-state))
315 (darkroom--saved-state
318 ;; for clarity, don't do anything
321 (define-minor-mode darkroom-mode
322 "Remove visual distractions and focus on writing. When this
323 mode is active, everything but the buffer's text is elided from
324 view. The buffer margins are set so that text is centered on
325 screen. Text size is increased (display engine allowing) by
326 `darkroom-text-scale-increase'." nil nil nil
327 (when darkroom-tentative-mode
330 (concat "Turning off `darkroom-tentative-mode' first. "
331 "It doesn't go with `darkroom-mode'.")
332 (let ((darkroom-mode nil))
333 (darkroom-tentative-mode -1))))
336 (add-hook 'window-configuration-change-hook 'darkroom--set-margins
340 (remove-hook 'window-configuration-change-hook 'darkroom--set-margins
343 (define-minor-mode darkroom-tentative-mode
344 "Enters `darkroom-mode' when all other windows are deleted."
345 nil " Room" darkroom-mode-map
346 ;; always begin by removing the hook
348 (remove-hook 'window-configuration-change-hook
349 'darkroom--enter-or-leave 'local)
353 (concat "Turning off `darkroom-mode' first. "
354 "It doesn't go with `darkroom-tentative-mode'.")
355 (let ((darkroom-tentative-mode nil))
356 (darkroom-mode -1))))
357 ;; turn darkroom on or off according to window state
359 (cond (darkroom-tentative-mode
360 ;; re-add the hook when we are turning ourselves on
362 (add-hook 'window-configuration-change-hook
363 'darkroom--enter-or-leave 'append 'local)
364 ;; call this right away if we're supposed to turn darkroom on
367 (darkroom--enter-or-leave))
374 ;;; darkroom.el ends here