Commit 33183a7e6ca3
Changed files (1)
lisp
lisp/eldoro.el
@@ -1,616 +0,0 @@
-;;; eldoro.el -- A pomodoro timer/tracker that works with org-mode. -*- lexical-binding: t -*-
-
-;; Copyright (C) 2012-2014 Peter Jones <pjones@pmade.com>
-;;
-;; Author: Peter Jones <pjones@pmade.com>
-;; URL: https://github.com/pjones/eldoro
-;; Version: 0.1.0
-;;
-;; This file is not part of GNU Emacs.
-
-;;; Commentary:
-;;
-;; Eldoro is a simple Pomodoro timer and tracker for Emacs. You
-;; define your tasks in an org-mode buffer then start Eldoro which
-;; helps you see your estimates and pomodori, along with a clock and
-;; notification system.
-
-;;; License:
-;;
-;; Permission is hereby granted, free of charge, to any person obtaining
-;; a copy of this software and associated documentation files (the
-;; "Software"), to deal in the Software without restriction, including
-;; without limitation the rights to use, copy, modify, merge, publish,
-;; distribute, sublicense, and/or sell copies of the Software, and to
-;; permit persons to whom the Software is furnished to do so, subject to
-;; the following conditions:
-;;
-;; The above copyright notice and this permission notice shall be
-;; included in all copies or substantial portions of the Software.
-;;
-;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-;; LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-;; OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-;; WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-;;; Code:
-(eval-when-compile
- (require 'org)
- (require 'org-clock))
-
-(defgroup eldoro nil
- "A pomodoro timer that works with `org-mode'."
- :version "0.1.0"
- :prefix "eldoro-"
- :group 'applications)
-
-(defcustom eldoro-work-time 25
- "The number of minutes that a pomodoro working block takes."
- :type 'integer
- :group 'eldoro)
-
-(defcustom eldoro-short-break 5
- "The number of minutes that a short break lasts."
- :type 'integer
- :group 'eldoro)
-
-(defcustom eldoro-long-break 20
- "The number of minutes that a long break lasts."
- :type 'integer
- :group 'eldoro)
-
-(defcustom eldoro-long-break-after 4
- "The number of pomodori after which a long break is taken."
- :type 'integer
- :group 'eldoro)
-
-(defcustom eldoro-show-help t
- "Whether to show a short help message in the Eldoro buffer or not."
- :type 'boolean
- :group 'eldoro)
-
-(defcustom eldoro-current-task-prompt " > "
- "The string to place in front of the active task."
- :type 'string
- :group 'eldoro)
-
-(defcustom eldoro-date-format "%A, %B %d, %Y"
- "The date format used for pomodoro statistics."
- :type 'string
- :group 'eldoro)
-
-(defcustom eldoro-notify-function 'org-notify
- "A function to call to notify the user that a pomodoro or break
-has expired. The function should take a single argument, a
-string to display to the user."
- :type 'function
- :group 'eldoro)
-
-(defcustom eldoro-pomodoro-end-msg
- "The current pomodoro for \"%s\" has ended. Time for a break!"
- "A notification message shown when a pomodoro has ended. The
-string is run through `format' with one string argument, the
-title of the current task."
- :type 'string
- :group 'eldoro)
-
-(defcustom eldoro-break-end-msg
- "The current break has ended. Get back to work!"
- "A notification message shown when a break has ended. The
-string is run through `format' with one string argument, the
-title of the current task."
- :type 'string
- :group 'eldoro)
-
-(defcustom eldoro-use-org-clock nil
- "If non-nil, start the org mode clock when starting a pomodoro
-and stop it for an interruption or break."
- :type 'boolean
- :group 'eldoro)
-
-(defcustom eldoro-record-in-properties t
- "If non-nil, record the number of pomodori and interruptions
-into the source org buffer using properties."
- :type 'boolean
- :group 'eldoro)
-
-(defcustom eldoro-estimate-property "ELDORO_ESTIMATE"
- "The name of the `org-mode' property in which to read pomodoro
-estimates."
- :type 'string
- :group 'eldoro)
-
-(defcustom eldoro-pomodoro-property "ELDORO_POMODORI"
- "The name of the `org-mode' property in which to store pomodoro
-counts."
- :type 'string
- :group 'eldoro)
-
-(defcustom eldoro-interruption-property "ELDORO_INTERRUPTIONS"
- "The name of the `org-mode' property in which to store
-interruption counts."
- :type 'string
- :group 'eldoro)
-
-(defgroup eldoro-faces nil
- "Customize the appearance of Eldoro."
- :prefix "eldoro-"
- :group 'faces
- :group 'eldoro)
-
-(defface eldoro-header
- '((t :inherit header-line))
- "Face for the header lines in the Eldoro buffer."
- :group 'eldoro-faces)
-
-(defface eldoro-active-task
- '((t :inherit highlight))
- "Face for the active task in Eldoro."
- :group 'eldoro-faces)
-
-;;; Internal Variables.
-(defvar eldoro-buffer-name "*Eldoro*"
- "The name of the buffer used to show pomodoros.")
-
-(defvar eldoro--show-help nil)
-(defvar eldoro--start-time nil)
-(defvar eldoro--source-marker nil)
-(defvar eldoro--active-marker nil)
-(defvar eldoro--countdown-type nil)
-(defvar eldoro--countdown-start nil)
-(defvar eldoro--pomodori 0)
-(defvar eldoro--breaks 0)
-(defvar eldoro--interrupts 0)
-(defvar eldoro--leave-point 0)
-(defvar eldoro--first-task 0)
-(defvar eldoro--timer nil)
-(defvar eldoro--sent-notification nil)
-(defvar eldoro--skip-update nil)
-
-(defvar eldoro-mode-map
- (let ((map (make-sparse-keymap)))
- (define-key map (kbd "b") 'bury-buffer)
- (define-key map (kbd "g") 'eldoro-update)
- (define-key map (kbd "h") 'eldoro-toggle-help)
- (define-key map (kbd "i") 'eldoro-interruption)
- (define-key map (kbd "q") 'eldoro-quit)
- (define-key map (kbd "r") 'eldoro-reset-statistics)
- (define-key map (kbd "s") 'eldoro-stop-clock)
- (define-key map (kbd "TAB") 'eldoro-jump-to-heading)
- (define-key map (kbd "RET") 'eldoro-next-action)
- map)
- "Keymap used in Eldoro mode.")
-
-;;;###autoload
-(defun eldoro (&optional force-reset)
- "Start Eldoro on the current `org-mode' heading. If Eldoro is
-already running bring its buffer forward.
-
-If Eldoro has already been started and this function is called
-from an `org-mode' buffer, prompt for permission to reset the
-Eldoro tasks. With a prefix argument force a reset without
-prompting."
- (interactive)
- (cond
- ;; We're in an org buffer and we're allowed to reset.
- ((and (string= major-mode "org-mode")
- (or (not eldoro--source-marker) force-reset
- (y-or-n-p "Reset Eldoro from this org buffer? ")))
- (if eldoro--countdown-start (eldoro-stop-clock))
- (eldoro-reset-vars)
- (save-excursion
- (org-back-to-heading)
- (setq eldoro--source-marker (point-marker)))
- (unless (> (eldoro-children-count) 0)
- (error "This heading doesn't have any children"))
- (switch-to-buffer (get-buffer-create eldoro-buffer-name))
- (eldoro-mode))
- ;; There's an Eldoro buffer already.
- ((get-buffer eldoro-buffer-name)
- (switch-to-buffer eldoro-buffer-name))
- ;; No Eldoro buffer, and we're not in an org-mode buffer.
- (t
- (error "Eldoro mode should be started from an org-mode heading"))))
-
-(define-derived-mode eldoro-mode fundamental-mode "Eldoro"
- "A major mode for showing pomodoro timers.
-
-Eldoro works with tasks defined in an `org-mode' buffer. When
-you start Eldoro from within an `org-mode' buffer it will gather
-all of headings which are children of the heading that point is
-currently on. You can then start and stop pomodoro and break
-timers from within the Eldoro buffer.
-
-## Getting Started
-
-First, create an `org-mode' outline that looks something like
-this:
-
-~~~
-* Tasks I Want To Do
-** Write angry letter to Congress
-** Find a bug in OpenSSL
-** Upload compromising photos to FB
-~~~
-
-Now, move point to the first-level heading and start Eldoro with
-the `eldoro' interactive function. Move point to a task you want
-to work on and press RET.
-
-To switch to a break, just press RET again. If someone
-interrupts you, press i.
-
-
-
-## Reporting
-
-By default, Eldoro writes some basic statistics into `org-mode'
-properties. If you want to compare the number of pomodori from
-day to day make sure you create new headings in the `org-mode'
-buffer every day. Eldoro does not currently record timestamps
-with its statistics. It would be nice if there was a better Org
-API for logging Eldoro statistics into a drawer."
- :group 'eldoro
- (setq buffer-read-only t
- truncate-lines t)
- (buffer-disable-undo)
- (eldoro-timer-stop)
- (setq eldoro--timer (run-at-time nil 10 'eldoro-update)))
-
-(defun eldoro-update ()
- "Update the Eldoro buffer."
- (interactive)
- (unless eldoro--skip-update
- (when (not (string= (format-time-string "%Y%m%d" eldoro--start-time)
- (format-time-string "%Y%m%d")))
- (setq eldoro--start-time (current-time))
- (eldoro-really-reset-counters))
- (let ((buffer (get-buffer eldoro-buffer-name)))
- (if (not buffer) (progn (eldoro-timer-stop) (eldoro-reset-vars))
- (with-current-buffer buffer
- (let ((buffer-read-only nil)) (eldoro-draw-buffer)))))
- (if (and eldoro--countdown-start (<= (eldoro-remaining) 0))
- (eldoro-send-notification))))
-
-(defun eldoro-quit ()
- "Stop the current timer and kill the Eldoro buffer."
- (interactive)
- (when (y-or-n-p "Really quit Eldoro? ")
- (if eldoro--countdown-start (eldoro-stop-clock))
- (eldoro-timer-stop)
- (eldoro-reset-vars)
- (kill-buffer eldoro-buffer-name)))
-
-(defun eldoro-next-action ()
- "Start the next appropriate clock (pomodoro or break)."
- (interactive)
- (let ((eldoro--skip-update t)
- (old eldoro--countdown-type)
- (marker (eldoro-task-p))
- (old-marker eldoro--active-marker))
- (if (not marker) (error "Please move point to a task first"))
-
- ;; Clock is running, figure out what to do.
- (when eldoro--countdown-start
- (eldoro-stop-clock)
-
- ;; If we're on the same task that was previously running then
- ;; reset the counter type so that starting the clock switches to
- ;; a break.
- (if (and marker (equal marker old-marker))
- (setq eldoro--countdown-type old)))
-
- ;; Restart the clock;
- (eldoro-start-clock))
-
- ;; Now update the display.
- (eldoro-update))
-
-(defun eldoro-start-clock ()
- "Start a pomodoro or break clock for the task at point."
- (interactive)
- (if eldoro--countdown-start (error "A task is still in progress"))
- (let ((marker (eldoro-task-p)))
- (if (not marker) (error "Please move point to a task first"))
- (setq eldoro--active-marker marker
- eldoro--countdown-start (float-time)
- eldoro--countdown-type (eldoro-next-clock-type)
- eldoro--sent-notification nil)
- (if (eq eldoro--countdown-type 'work) (eldoro-org-clock-start)))
- (eldoro-update))
-
-(defun eldoro-stop-clock (&optional interruption)
- "Stop the current pomodoro/break clock. With a prefix argument
-abort the current pomodoro due to an interruption."
- (interactive "P")
- (if (not eldoro--countdown-start) (error "No task is in progress"))
- (eldoro-org-clock-stop)
- (let ((restarting nil))
- (cond
- ;; Stop working (not an interruption).
- ((and (eq eldoro--countdown-type 'work) (not interruption))
- (eldoro-record-pomodoro)
- (setq eldoro--pomodori (1+ eldoro--pomodori)))
- ;; Restart work timer due to an interruption.
- ((eq eldoro--countdown-type 'work)
- (eldoro-record-interruption)
- (setq eldoro--interrupts (1+ eldoro--interrupts)))
- ;; Stop during a break (not an interruption).
- ((and (eq eldoro--countdown-type 'break) (not interruption))
- (setq eldoro--breaks (1+ eldoro--breaks)))
- ;; Restart a break due to an interruption.
- ((eq eldoro--countdown-type 'break)
- (setq eldoro--countdown-start (float-time)
- eldoro--sent-notification nil
- restarting t)))
- (unless restarting
- (setq eldoro--countdown-start nil
- eldoro--countdown-type nil
- eldoro--active-marker nil))
- (eldoro-update)))
-
-(defun eldoro-interruption ()
- "Abort the current pomodoro due to an interruption and start a
-new pomodoro."
- (interactive)
- (if eldoro--countdown-start (eldoro-stop-clock t)))
-
-(defun eldoro-jump-to-heading ()
- "With point on an Eldoro task, make the source org buffer
-current and jump to the matching heading."
- (interactive)
- (let ((marker (eldoro-task-p)))
- (if (not marker) (error "Please move point to an Eldoro task"))
- (switch-to-buffer (marker-buffer marker))
- (goto-char (marker-position marker))))
-
-(defun eldoro-reset-statistics (&optional force)
- "Reset the counters used to track pomodori, breaks, and
-interruptions. With a prefix argument don't prompt for
-confirmation."
- (interactive "P")
- (when (or force (y-or-n-p "Really reset Eldoro counters? "))
- (eldoro-really-reset-counters)
- (eldoro-update)))
-
-(defun eldoro-toggle-help ()
- "Toggle whether or not a brief help message is displayed in the
-Eldoro buffer."
- (interactive)
- (setq eldoro--show-help (not eldoro--show-help))
- (eldoro-update))
-
-;;;-------------------------------------------------------------------------
-;;; Internal Functions.
-;;;-------------------------------------------------------------------------
-
-;; Silence a compiler warning
-(declare-function org-clock-in "org-clock")
-(declare-function org-clock-out "org-clock")
-
-(defun eldoro-map-tree (eldoro-fun)
- "Call ELDORO-FUN for each child in the org source tree."
- (let ((start-level))
- (with-current-buffer (marker-buffer eldoro--source-marker)
- (save-excursion
- (goto-char (marker-position eldoro--source-marker))
- (setq start-level (funcall outline-level)) ; Why funcall?
- (org-map-tree
- (lambda () (if (/= start-level (funcall outline-level))
- (funcall eldoro-fun))))))))
-
-(defun eldoro-children-count ()
- "Return the number of child headings in the org doc."
- (let ((children 0))
- (eldoro-map-tree (lambda () (setq children (1+ children))))
- children))
-
-(defun eldoro-task-p ()
- "Return nil if point isn't on a Eldoro task, otherwise returns
-the marker associated with the task at point."
- (with-current-buffer eldoro-buffer-name
- (get-text-property (point) 'eldoro-src)))
-
-(defun eldoro-minutes-as-string (minutes)
- (if (= (abs minutes) 1) "minute" "minutes"))
-
-(defun eldoro-remaining-string (&optional countdown)
- (let* ((time (or countdown eldoro--countdown-start))
- (min (eldoro-remaining time))
- (ajd (if (>= min 0) " remaining" " too long"))
- (clock (number-to-string (abs min))))
- (concat clock " " (eldoro-minutes-as-string min) ajd)))
-
-(defun eldoro-remaining (&optional countdown)
- (setq countdown (or countdown eldoro--countdown-start))
- (round (- (eldoro-duration)
- (/ (- (float-time) countdown) 60))))
-
-(defun eldoro-duration ()
- "Return the number of minutes the clock should run for."
- (cond
- ((eq eldoro--countdown-type 'work)
- eldoro-work-time)
- ((and (eq eldoro--countdown-type 'break)
- (/= eldoro--pomodori 0)
- (= (% eldoro--pomodori eldoro-long-break-after) 0))
- eldoro-long-break)
- (t eldoro-short-break)))
-
-(defun eldoro-timer-stop ()
- "Stop the internal Emacs timer."
- (if eldoro--timer (setq eldoro--timer (cancel-timer eldoro--timer))))
-
-(defun eldoro-reset-vars ()
- "Reset all internal variables tied to a given org file."
- (if eldoro--countdown-start (eldoro-stop-clock))
- (if (not eldoro--start-time) (setq eldoro--start-time (current-time)))
- (setq eldoro--show-help eldoro-show-help
- eldoro--countdown-type nil
- eldoro--countdown-start nil
- eldoro--sent-notification nil
- eldoro--leave-point 0
- eldoro--source-marker nil
- eldoro--active-marker nil))
-
-(defun eldoro-really-reset-counters ()
- (setq eldoro--pomodori 0
- eldoro--breaks 0
- eldoro--interrupts 0))
-
-(defun eldoro-next-clock-type ()
- (cond
- ((eq eldoro--countdown-type 'work) 'break)
- ((eq eldoro--countdown-type 'break) 'work)
- (t 'work)))
-
-(defun eldoro-draw-buffer ()
- "Write the contents of the Eldoro buffer."
- (let ((buf (get-buffer eldoro-buffer-name))
- (size-before (buffer-size))
- (eldoro--leave-point (point))
- (eldoro--first-task 0))
- (erase-buffer)
- (eldoro-draw-stats)
- (if eldoro--show-help (eldoro-draw-help))
- (insert (propertize (concat (eldoro-parent-task-heading) ":")
- 'face 'eldoro-header))
- (insert "\n\n")
- (eldoro-map-tree 'eldoro-draw-heading)
- (set-buffer-modified-p nil)
- (setq eldoro--leave-point
- (if (= 0 size-before) eldoro--first-task
- (+ eldoro--leave-point (- (buffer-size) size-before))))
- (goto-char eldoro--leave-point)
- (dolist (w (window-list))
- (if (eq (window-buffer w) buf)
- (set-window-point w eldoro--leave-point)))))
-
-(defun eldoro-draw-stats ()
- (let ((indent (make-string (length eldoro-current-task-prompt) ? ))
- (clock (and eldoro--countdown-start (eldoro-remaining-string)))
- (pomodori (number-to-string eldoro--pomodori))
- (breaks (number-to-string eldoro--breaks))
- (interrupts (number-to-string eldoro--interrupts)))
- (insert (propertize (concat "Pomodoro statistics for "
- (format-time-string eldoro-date-format) ":")
- 'face 'eldoro-header))
- (insert "\n\n")
- (if eldoro--countdown-start
- (cond
- ((eq eldoro--countdown-type 'work)
- (insert (concat indent " Pomodoro Timer: " clock "\n")))
- ((eq eldoro--countdown-type 'break)
- (insert (concat indent " Break Timer: " clock "\n")))))
- (insert (concat indent " Pomodori: " pomodori "\n"))
- (insert (concat indent " Breaks: " breaks "\n"))
- (insert (concat indent " Interruptions: " interrupts "\n"))
- (insert "\n")))
-
-(defun eldoro-draw-help ()
- (let ((help (substitute-command-keys "\\{eldoro-mode-map}"))
- (indent (make-string (length eldoro-current-task-prompt) ? ))
- (offset 0))
- (while (string-match "^\\([^[:space:]]\\)" help offset)
- (setq help (replace-match (concat indent "\\1") t nil help))
- (setq offset (+ 2 offset)))
- (insert (propertize "Eldoro Help:" 'face 'eldoro-header))
- (insert (concat "\n\n" help))))
-
-(defun eldoro-draw-heading ()
- (let* ((heading (substring-no-properties (org-get-heading t t)))
- (mark (point-marker))
- (prompt (make-string (length eldoro-current-task-prompt) ? ))
- (estimate (eldoro-get-org-prop eldoro-estimate-property "0" mark))
- (done (eldoro-get-org-prop eldoro-pomodoro-property "0" mark))
- (stats (format "[%02d/%02d] "
- (string-to-number done)
- (string-to-number estimate)))
- task active)
- (if (equal mark eldoro--active-marker)
- (setq prompt eldoro-current-task-prompt active t))
- (setq task (concat prompt stats heading "\n"))
- (put-text-property 0 (length task) 'eldoro-src mark task)
- (with-current-buffer eldoro-buffer-name
- (if (= eldoro--first-task 0) (setq eldoro--first-task (point)))
- (if active (insert (propertize task 'face 'eldoro-active-task))
- (insert task)))))
-
-(defun eldoro-at-marker (marker fun)
- "Move to MARKER and apply FUN."
- (setq marker (or marker eldoro--active-marker))
- (with-current-buffer (marker-buffer marker)
- (save-excursion
- (goto-char (marker-position marker))
- (funcall fun))))
-
-(defun eldoro-get-task-heading (&optional marker)
- "Return the heading text for the heading at MARKER or at the
-active marker if MARKER is nil."
- (eldoro-at-marker
- marker
- (lambda () (substring-no-properties (org-get-heading t t)))))
-
-(defun eldoro-parent-task-heading ()
- "Return the heading text for the task Eldoro was started on."
- (eldoro-get-task-heading eldoro--source-marker))
-
-(defun eldoro-active-task-heading ()
- "Return the heading text for the active task."
- (eldoro-get-task-heading eldoro--active-marker))
-
-(defun eldoro-record-pomodoro ()
- "Increment the number of pomodori for the active task."
- (if eldoro-record-in-properties
- (eldoro-inc-org-prop eldoro-pomodoro-property)))
-
-(defun eldoro-record-interruption ()
- "Increment the number of interruptions for the active task."
- (if eldoro-record-in-properties
- (eldoro-inc-org-prop eldoro-interruption-property)))
-
-(defun eldoro-get-org-prop (name &optional missing marker)
- "Return the value for the given property. If the property is
-missing return the value of MISSING. By default the property is
-looked up on the active org heading unless MARKER is given."
- (eldoro-at-marker
- marker (lambda () (or (org-entry-get (point) name) missing))))
-
-(defun eldoro-set-org-prop (name value &optional marker)
- "Set the property NAME to VALUE for MARKER or the active
-heading."
- (eldoro-at-marker
- marker (lambda () (org-entry-put (point) name value))))
-
-(defun eldoro-inc-org-prop (name &optional marker)
- (let* ((s (eldoro-get-org-prop name "0" marker))
- (n (1+ (string-to-number s))))
- (eldoro-set-org-prop name (number-to-string n) marker)))
-
-(defun eldoro-org-clock-start ()
- "Start the `org-mode' clock for the active heading."
- (when eldoro-use-org-clock
- (eldoro-at-marker
- eldoro--active-marker (lambda () (org-clock-in)))))
-
-(defun eldoro-org-clock-stop ()
- "Stop a running `org-mode' clock."
- (when (and eldoro-use-org-clock eldoro--countdown-start)
- (eldoro-at-marker
- eldoro--active-marker (lambda () (org-clock-out t)))))
-
-(defun eldoro-send-notification ()
- "Send a notification that a pomodoro or break ended."
- (when (and (not eldoro--sent-notification) eldoro-notify-function)
- (setq eldoro--sent-notification t)
- (let ((msg (if (eq eldoro--countdown-type 'work)
- eldoro-pomodoro-end-msg
- eldoro-break-end-msg)))
- (funcall eldoro-notify-function
- (format msg (eldoro-active-task-heading))))))
-
-(provide 'eldoro)
-;;; eldoro.el ends here