;;; golden-ratio.el --- Automatic resizing of Emacs windows to the golden ratio

;; Copyright (C) 2012 Roman Gonzalez

;; Author: Roman Gonzalez <romanandreg@gmail.com>
;; Mantainer: Roman Gonzalez <romanandreg@gmail.com>
;; Created: 13 Oct 2012
;; Keywords: Window Resizing
;; Version: 0.0.4

;; Code inspired by ideas from Tatsuhiro Ujihisa

;; This file is not part of GNU Emacs.

;; This file is free software (MIT License)

;;; Code:
(eval-when-compile (require 'cl))

(defconst golden-ratio--value 1.618
  "The golden ratio value itself.")

(defgroup golden-ratio nil
  "Resize windows to golden ratio."
  :group 'windows)

;; Major modes that are exempt from being resized. An example of this
;; for users of Org-mode might be:
;;  ("calendar-mode") or (calendar-mode)
(defcustom golden-ratio-exclude-modes nil
  "A list of symbols or strings naming major modes.
Switching to a buffer whose major mode is a member of this list
will not cause the window to be resized to the golden ratio."
  :type '(repeat (choice symbol string))
  :group 'golden-ratio)

;; Buffer names that are exempt from being resized. An example of this
;; for users of Org-mode might be (note the leading spaces):
;;  (" *Org tags*" " *Org todo*")
(defcustom golden-ratio-exclude-buffer-names nil
  "An array of strings containing buffer names.
Switching to a buffer whose name is a member of this list
will not cause the window to be resized to the golden ratio."
  :type '(repeat string)
  :group 'golden-ratio)

(defcustom golden-ratio-inhibit-functions nil
  "List of functions to call with no arguments.
Switching to a buffer, if any of these functions returns non-nil
will not cause the window to be resized to the golden ratio."
  :group 'golden-ratio
  :type '(repeat symbol))

(defcustom golden-ratio-extra-commands
  '(windmove-left windmove-right windmove-down windmove-up)
  "List of extra commands used to jump to other window."
  :group 'golden-ratio
  :type '(repeat symbol))

(defcustom golden-ratio-recenter nil
  "Recenter window vertically and scroll right when non--nil."
  :group 'golden-ratio
  :type 'boolean)

(defcustom golden-ratio-adjust-factor 1.0
  "Adjust the width sizing by some factor. 1 is no adjustment.
   For very wide screens/frames, ie. 3400px, .4 may work well."
  :group 'golden-ratio
  :type 'integer)

(defcustom golden-ratio-wide-adjust-factor 0.8
  "Width adjustment factor for widescreens. Used when
   toggling between widescreen and regular modes."
  :group 'golden-ratio
  :type 'float)

(defcustom golden-ratio-auto-scale nil
  "Automatic width adjustment factoring. Scales the width
   of the screens to be smaller as the frame gets bigger."
  :group 'golden-ratio
  :type 'boolean)

(defcustom golden-ratio-max-width nil
  "Set a maximum column width on the active window."
  :group 'golden-ratio
  :type 'integer)

(defcustom golden-ratio-exclude-buffer-regexp nil
  "A list of regexp's used to match buffer names.
Switching to a buffer whose name matches one of these regexps
will prevent the window to be resized to the golden ratio."
  :type '(repeat string)
  :group 'golden-ratio)

;;; Compatibility
;;
(unless (fboundp 'window-resizable-p)
  (defalias 'window-resizable-p 'window--resizable-p))

(defun golden-ratio-toggle-widescreen ()
  (interactive)
  (if (= golden-ratio-adjust-factor 1)
      (setq golden-ratio-adjust-factor golden-ratio-wide-adjust-factor)
    (setq golden-ratio-adjust-factor 1))
  (golden-ratio))

(defun golden-ratio-adjust (a)
  "set the adjustment of window widths."
  (interactive
   (list
    (read-number "Screeen width adjustment factor: " golden-ratio-adjust-factor)))
  (setq golden-ratio-adjust-factor a)
  (golden-ratio))

(defun golden-ratio--scale-factor ()
  (if golden-ratio-auto-scale
      (- 1.0 (* (/ (- (frame-width) 100.0) 1000.0) 1.8))
    golden-ratio-adjust-factor))

(defun golden-ratio--dimensions ()
  (list (floor (/ (frame-height) golden-ratio--value))
        (let ((width (floor  (* (/ (frame-width)  golden-ratio--value)
                                (golden-ratio--scale-factor)))))
          (if golden-ratio-max-width
              (min golden-ratio-max-width width)
            width))))

(defun golden-ratio--resize-window (dimensions &optional window)
  (with-selected-window (or window (selected-window))
    (let ((nrow  (floor (- (first  dimensions) (window-height))))
          (ncol  (floor (- (second dimensions) (window-width)))))
      (when (window-resizable-p (selected-window) nrow)
        (enlarge-window nrow))
      (when (window-resizable-p (selected-window) ncol t)
        (enlarge-window ncol t)))))

(defun golden-ratio-exclude-major-mode-p ()
  "Returns non-nil if `major-mode' should not use golden-ratio."
  (or (memq major-mode golden-ratio-exclude-modes)
      (member (symbol-name major-mode)
              golden-ratio-exclude-modes)))

;;;###autoload
(defun golden-ratio (&optional arg)
  "Resizes current window to the golden-ratio's size specs."
  (interactive "p")
  (unless (or (and (not golden-ratio-mode) (null arg))
              (window-minibuffer-p)
              (one-window-p)
              (golden-ratio-exclude-major-mode-p)
              (member (buffer-name)
                      golden-ratio-exclude-buffer-names)
              (and golden-ratio-exclude-buffer-regexp
                (loop for r in golden-ratio-exclude-buffer-regexp
                         thereis (string-match r (buffer-name))))
              (and golden-ratio-inhibit-functions
                   (loop for fun in golden-ratio-inhibit-functions
                         thereis (funcall fun))))
    (let ((dims (golden-ratio--dimensions))
          (golden-ratio-mode nil))
      ;; Always disable `golden-ratio-mode' to avoid
      ;; infinite loop in `balance-windows'.
      (let (window-configuration-change-hook)
        (balance-windows)
        (golden-ratio--resize-window dims)
        (when golden-ratio-recenter
          (scroll-right) (recenter)))
      (run-hooks 'window-configuration-change-hook))))

;; Should return nil
(defadvice other-window
    (after golden-ratio-resize-window)
  (golden-ratio) nil)

;; Should return the buffer
(defadvice pop-to-buffer
    (around golden-ratio-resize-window)
  (prog1 ad-do-it (golden-ratio)))

(defun golden-ratio--post-command-hook ()
  (when (or (memq this-command golden-ratio-extra-commands)
            (and (consp this-command) ; A lambda form.
                 (loop for com in golden-ratio-extra-commands
                       thereis (or (member com this-command)
                                   (member (car-safe com) this-command)))))
    ;; This is needed in emacs-25 to avoid this error from `recenter':
    ;; `recenter'ing a window that does not display current-buffer.
    ;; This doesn't happen in emacs-24.4 and previous versions.
    (run-with-idle-timer 0.01 nil (lambda () (golden-ratio)))))

(defun golden-ratio--mouse-leave-buffer-hook ()
  (run-at-time 0.1 nil (lambda ()
			 (golden-ratio))))

;;;###autoload
(define-minor-mode golden-ratio-mode
    "Enable automatic window resizing with golden ratio."
  :lighter " Golden"
  :global t
  (if golden-ratio-mode
      (progn
        (add-hook 'window-configuration-change-hook 'golden-ratio)
        (add-hook 'post-command-hook 'golden-ratio--post-command-hook)
        (add-hook 'mouse-leave-buffer-hook 'golden-ratio--mouse-leave-buffer-hook)
        (ad-activate 'other-window)
        (ad-activate 'pop-to-buffer))
      (remove-hook 'window-configuration-change-hook 'golden-ratio)
      (remove-hook 'post-command-hook 'golden-ratio--post-command-hook)
      (remove-hook 'mouse-leave-buffer-hook 'golden-ratio--mouse-leave-buffer-hook)
      (ad-deactivate 'other-window)
      (ad-deactivate 'pop-to-buffer)))


(provide 'golden-ratio)

;;; golden-ratio.el ends here
