You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
748 lines
25 KiB
748 lines
25 KiB
;;; -*- lexical-binding: t -*-
|
|
;;; tacc.el --- A pomidoro timer supporting tags and customizable export
|
|
|
|
;; Author: Brady McDonough <me@bradymcd.ca>
|
|
;; URL: git.bradymcd.ca/brady/tacc
|
|
;; Keywords: tools, time, pomidoro, accountability
|
|
;; Version: 0.0.1
|
|
;;
|
|
|
|
;; This program is free software; you can redistribute it and/or modify
|
|
;; it under the terms of the GNU General Public License as published by
|
|
;; the Free Software Foundation; either version 2, or version 3 at your
|
|
;; option.
|
|
;;
|
|
;; This program is distributed in the hope that it will be useful,
|
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
;; GNU General Public License for more details.
|
|
;;
|
|
;; You should have received a copy of the GNU General Public License
|
|
;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
;;; Commentary:
|
|
|
|
;; This implements a slightly modified pomidoro timer with a focus on
|
|
;; programmer customizability.
|
|
;; By default it is just a timer and the serialization and recording of your
|
|
;; time data is up to you (if that's something you even need)
|
|
;; Each successive work and break timer may be longer than the last. Work time
|
|
;; can also be tagged for better time accounting.
|
|
|
|
;;; Index:
|
|
;; 01.1 Indexed settings variables
|
|
;; 01.2 Indexed face settings
|
|
;; 02 Error types
|
|
;; 03 Behaviour group
|
|
;; 03.1 Tag prompt text completion
|
|
;; 03.2 Sound
|
|
;; 04 Record keeping
|
|
;; 05 Timer display buffer
|
|
;; 05.2 State checks
|
|
;; 05.3 State manipulation
|
|
;; 05.4 Timer buffer rendering
|
|
;; 07 Initialization
|
|
;; 08.1 Interactive helpers
|
|
;; 08.2 Interactive functions
|
|
;; 09 Hooks registration
|
|
;; 10 Provides
|
|
|
|
;;; Code:
|
|
|
|
;; TODOS: tacc-running? needs to be replaced by tacc-session? and tacc-live? depending on context
|
|
;;
|
|
;; ERROR: timer shutdown seems to leave things in a wrong state and buffer doesn't die properly
|
|
;; Check on the logic of push-current, needs predicates rather than lazy if conditions
|
|
;; I'm not calling anything to destroy the buffer on desched
|
|
;; BUG: The buffer is immortal?
|
|
;;
|
|
|
|
;; Optional
|
|
(require 'icicles nil t)
|
|
|
|
;; 01.1 Customs and Indexed Variables
|
|
(defconst tacc-dir (file-name-directory load-file-name))
|
|
|
|
(defgroup tacc nil
|
|
"Root group definition for tacc"
|
|
:prefix "tacc-"
|
|
:group 'applications)
|
|
|
|
(defcustom tacc-buffer-name "*Work Timer*"
|
|
"The name used for the timer display buffer"
|
|
:type 'string :group 'tacc)
|
|
|
|
(defcustom tacc-suppress-tag-prompt t
|
|
"If set nil tacc will ask for a new tag every time a work period begins"
|
|
:type 'boolean :group 'tacc)
|
|
|
|
(defvar tacc-display-buffer-settings '((display-buffer-below-selected)
|
|
(inhibit-same-window . t)
|
|
(dedicated . t))
|
|
"Passed into display-buffer. How a window should be created or reused")
|
|
|
|
(defvar tacc-display-buffer-window-settings '((window-min-height . 10)
|
|
(window-height . 0.25))
|
|
"Passed into display-buffer. The window's overall size")
|
|
|
|
(defun m:s (mins secs)
|
|
"Readability function for minute and second values"
|
|
(+ secs (* 60 mins)))
|
|
|
|
(defcustom tacc-work-seconds (m:s 30 0)
|
|
"The length of a base pomidoro round"
|
|
:type 'integer :group 'tacc)
|
|
|
|
(defcustom tacc-work-seconds-increment 0
|
|
"The time to add to each successive work cycle"
|
|
:type 'integer :group 'tacc)
|
|
|
|
(defcustom tacc-break-seconds (m:s 5 0)
|
|
"The length of a base break round"
|
|
:type 'integer :group 'tacc)
|
|
|
|
(defcustom tacc-break-seconds-increment 0
|
|
"The time to add to each successive break cycle"
|
|
:type 'integer :group 'tacc)
|
|
|
|
(defcustom tacc-chime-interval (m:s 0 27)
|
|
"The time between alerts for timers running past their length"
|
|
:type 'integer :group 'tacc)
|
|
|
|
;; 01.2 Faces
|
|
(defgroup tacc-faces nil
|
|
"Custom faces for tacc"
|
|
:group 'tacc
|
|
:group 'faces
|
|
:prefix "tacc-")
|
|
|
|
(defface tacc-graph-work
|
|
'((t :inherit success))
|
|
"For work graph bars"
|
|
:group 'tacc-faces)
|
|
|
|
(defface tacc-graph-break
|
|
'((t :inherit warning))
|
|
"For break graph bars"
|
|
:group 'tacc-faces)
|
|
|
|
(defface tacc-graph-void
|
|
'((t :inherit shadow))
|
|
"For filler graph bars"
|
|
:group 'tacc-faces)
|
|
|
|
(defface tacc-clock
|
|
'((t :height 3.0))
|
|
"For the clock and timer"
|
|
:group 'tacc-faces)
|
|
|
|
(defface tacc-clock-pause
|
|
'((t :inherit (tacc-clock italic ansi-slow-blink)))
|
|
"For the timer while the timer isn't running"
|
|
:group 'tacc-faces)
|
|
|
|
(defface tacc-overwork
|
|
'((t :inherit (tacc-clock error)))
|
|
"for the timer whenever we are in overwork"
|
|
:group 'tacc-faces)
|
|
|
|
(defface tacc-tag
|
|
'((t :inherit link-visited))
|
|
"The face the tag is printed in"
|
|
:group 'tacc-faces)
|
|
|
|
;; 02 Errors
|
|
(define-error 'tacc-error "Generic error from the tacc-timer module" 'error)
|
|
(define-error 'tacc-error-illegal-buffer-state
|
|
"Precondition failed, the timer buffer is in an invalid state" 'tacc-error)
|
|
(define-error 'tacc-error-serialization-registered
|
|
"Failed to register a serialization, serializations must have unique names"
|
|
'tacc-error)
|
|
|
|
;; 03 Behavior group
|
|
(defgroup tacc-behavior nil
|
|
"Low level behavior settings for tacc"
|
|
:group 'tacc
|
|
:prefix "tacc-")
|
|
|
|
;; 03.1 Tag prompts and completion
|
|
(defvar tacc-tag-history nil
|
|
"Tag name history, used for completions")
|
|
|
|
(defcustom tacc-tag-history-file
|
|
(expand-file-name "tags-history" tacc-dir)
|
|
"Tag history storage file, for completions"
|
|
:type '(file :must-match t)
|
|
:group 'tacc-behavior)
|
|
|
|
(defcustom tacc-tag-completion-functions nil
|
|
"A list of user defined functions for generating completions for tagging.
|
|
Each function must have no required arguments and return a list of strings"
|
|
:type 'hook
|
|
:group 'tacc-behavior)
|
|
|
|
(defun tacc-load-tag-history ()
|
|
"Load completions for tags"
|
|
(with-temp-buffer
|
|
(insert-file-contents tacc-tag-history-file)
|
|
(setq tacc-tag-history (split-string (buffer-string) "\n"))))
|
|
|
|
(defun tacc-save-tag-history ()
|
|
"Save completions for tags"
|
|
(with-temp-file tacc-tag-history-file
|
|
(insert (mapconcat 'identity
|
|
tacc-tag-history
|
|
"\n"))))
|
|
|
|
;; 03.2 Sound
|
|
(defcustom tacc-sound-tick
|
|
(expand-file-name "tick.wav" tacc-dir)
|
|
"The 'Tick' sound of the timer"
|
|
:type '(file :must-match t)
|
|
:group 'tacc-behavior)
|
|
|
|
(defcustom tacc-sound-chime
|
|
(expand-file-name "chime.wav" tacc-dir)
|
|
"The break and work alarm sound of the timer"
|
|
:type '(file :must-match t)
|
|
:group 'tacc-behavior)
|
|
|
|
(defun tacc-play-sound-file-emacs (file)
|
|
"Play some file in a child emacs process"
|
|
(if (fboundp 'play-sound-internal)
|
|
(start-process "tacc-play-sound"
|
|
nil
|
|
(car command-line-args)
|
|
"--batch"
|
|
"--eval" (format "(play-sound-file \"%s\")" file))
|
|
(warn "Emacs lacks builtin sound support")))
|
|
|
|
(defcustom tacc-play-sound-file #'tacc-play-sound-file-emacs
|
|
"Asyncronous function to play sounds. Set to nil to disable sound"
|
|
:type '(choice (const nil)
|
|
function)
|
|
:group 'tacc-behavior)
|
|
|
|
;; 04 Record keeping
|
|
(defvar tacc-serials '()
|
|
"A list of serialization functions keyed with names registered at runtime")
|
|
|
|
(defun tacc-register-serialization (name serialize)
|
|
"The supplied `serialize' function will be run every time a work session has
|
|
ended, either because `tacc-end-timer' was called by the user or when emacs
|
|
closes with a session still live. Supplied serialize function must take as an
|
|
argument a cons list of records.
|
|
A record is an alist layed out as follows:
|
|
`(:ts . <unix timestamp>)
|
|
(:end . <unix timestamp>)
|
|
(:state . <:break | :work>)
|
|
(:tag . <an assigned tag name>)'
|
|
The supplied `name' will be used to generate a new (empty) abnormal hook,
|
|
`tacc-<name>-output-functions'. Each function added to this hook will be
|
|
called with the output of the `serialize' function as an argument. Keep in
|
|
mind that this is *not* a copy, if your output function mutates its input for
|
|
any reason it should copy the argument first."
|
|
(let* ((sym-name (format "tacc-%s-output-functions" name)))
|
|
(if (intern-soft sym-name)
|
|
(error "tacc-register-serialization failed, it seems %s is already \
|
|
registered" sym-name)
|
|
(set (intern sym-name) '())
|
|
(push (cons name serialize) tacc-serials))))
|
|
|
|
(defun tacc-save-record (records)
|
|
"Calls all user-defined serializers and output functions"
|
|
(dolist (ser tacc-serials)
|
|
(let* ((sym-name (format "tacc-%s-output-functions" (car ser)))
|
|
(fn (cdr ser))
|
|
(data (funcall fn records)))
|
|
(run-hook-with-args (intern sym-name) data))))
|
|
|
|
;; 05 Timer display buffer
|
|
(defvar tacc-timer-mode-map
|
|
(let ((map (make-sparse-keymap)))
|
|
map))
|
|
|
|
(define-derived-mode tacc-timer-mode
|
|
special-mode tacc-buffer-name
|
|
"Major mode for the tacc timer"
|
|
(setq-local show-trailing-whitespace nil
|
|
tacc-locs `((clock-start . ,(copy-marker 0))
|
|
(clock-end . ,(copy-marker 0))
|
|
(graph-start . ,(copy-marker 0))
|
|
(graph-end . ,(copy-marker 0)))))
|
|
|
|
(defun tacc-info-initial ()
|
|
"Returns the initial state of tacc-info"
|
|
(copy-alist `((history . ())
|
|
(cycle . 0)
|
|
(work-ts . nil)
|
|
(break-ts . nil)
|
|
(end-ts . nil)
|
|
(state . stop)
|
|
(tag . ""))))
|
|
|
|
(defvar tacc-info (tacc-info-initial)
|
|
"State information related to the timer")
|
|
|
|
(defvar tacc-timer nil
|
|
"Timer object controlling tacc's second-by-second update")
|
|
|
|
;; 05.2 State checks
|
|
|
|
|
|
(defun tacc-buffer ()
|
|
"Gets the tacc-timer buffer if there is one"
|
|
(get-buffer tacc-buffer-name))
|
|
|
|
(defun tacc-live-state? (state)
|
|
"Checks if the state supplied indicates a live timer"
|
|
(cond ((eq state 'pause) nil)
|
|
((eq state 'stop) nil)
|
|
((eq state 'work) t)
|
|
((eq state 'break) t)
|
|
('t (signal 'tacc-error-illegal-buffer-state state))))
|
|
|
|
(defun tacc-live? ()
|
|
"Checks if there is a live timer running"
|
|
(timerp tacc-timer))
|
|
|
|
(defun tacc-session-state? (state)
|
|
"Checks if the state supplied indicates there should be session information"
|
|
(cond ((eq state 'pause) t)
|
|
((eq state 'stop) nil)
|
|
((eq state 'work) t)
|
|
((eq state 'break) t)
|
|
('t (signal 'tacc-error-illegal-buffer-state state))))
|
|
|
|
(defun tacc-session? ()
|
|
"Checks if tacc session info indicates there should be session information"
|
|
(tacc-session-state? (alist-get 'state tacc-info)))
|
|
|
|
(defun tacc-history? ()
|
|
"Checks if tacc session has history information"
|
|
(if (> 0 (length (alist-get 'history tacc-info)))
|
|
t
|
|
nil))
|
|
|
|
(defun tacc-invariant ()
|
|
"Returns t if tacc is in a legal state for mutating tacc-info, signal an error otherwise
|
|
Note, if there is no tacc-buffer then you shouldn't be touching tacc-info"
|
|
(if (bufferp (tacc-buffer))
|
|
(if (tacc-live?)
|
|
(if (tacc-live-state?)
|
|
t
|
|
;; Timer running without reason in logic
|
|
(signal 'tacc-error-illegal-buffer-state (tacc-buffer)))
|
|
(if (tacc-session-state?)
|
|
t
|
|
;; Buffer is live without reason in logic
|
|
(signal 'tacc-error-illegal-buffer-state (tacc-buffer))))
|
|
;; We shouldn't be calling this when the buffer is dead
|
|
(signal 'tacc-error-illegal-buffer-state (tacc-buffer))))
|
|
|
|
(defun tacc-cleanup-invariant ()
|
|
"Returns t if tacc is in a cleaned up state, signals an error otherwise"
|
|
(if (bufferp (tacc-buffer))
|
|
(signal 'tacc-dirty-state (tacc-buffer))
|
|
(if (tacc-live?)
|
|
(signal 'tacc-dirty-state tacc-timer)
|
|
(if (tacc-session?)
|
|
(signal 'tacc-dirty-state tacc-info)
|
|
t))))
|
|
|
|
;; 05.3 Predicates and helpers
|
|
(defun tacc-chime-p (countdown-ts)
|
|
"Should I play a chime on this second?"
|
|
(if (time-less-p countdown-ts 0)
|
|
(= 0 (mod (time-to-seconds countdown-ts)
|
|
tacc-chime-interval))))
|
|
|
|
(defun tacc-work-period (cycle)
|
|
"How long is the current work period?"
|
|
(+ tacc-work-seconds
|
|
(* cycle tacc-work-seconds-increment)))
|
|
|
|
(defun tacc-break-period (cycle)
|
|
"How long is the current break period?"
|
|
(+ tacc-break-seconds
|
|
(* cycle tacc-break-seconds-increment)))
|
|
|
|
(defun tacc-start-ts (state work-ts break-ts)
|
|
"Given the state, which timestamp did we start on?"
|
|
(if (eq state 'work) work-ts break-ts))
|
|
|
|
(defun tacc-est-ts (state cycle work-ts break-ts)
|
|
"Given the state and cycle, how long is the current time segment?"
|
|
(let ((start-ts (tacc-start-ts state work-ts break-ts)))
|
|
(time-add start-ts
|
|
(if (eq state 'work)
|
|
(tacc-work-period cycle)
|
|
(tacc-break-period cycle)))))
|
|
|
|
;; 05.3 State manipulation
|
|
|
|
(defun tacc-tick ()
|
|
"Time marches on. Called every second while a tacc timer is running"
|
|
(let* ((now (current-time))
|
|
(state (alist-get 'state tacc-info))
|
|
(cycle (alist-get 'cycle tacc-info))
|
|
(work-ts (alist-get 'work-ts tacc-info))
|
|
(break-ts (alist-get 'break-ts tacc-info))
|
|
(est (tacc-est-ts state cycle work-ts break-ts))
|
|
(countdown (time-subtract est now)))
|
|
(tacc-timer-buffer-update state est countdown now)
|
|
(if (functionp tacc-play-sound-file)
|
|
(cond ((tacc-chime-p countdown)
|
|
(funcall tacc-play-sound-file tacc-sound-chime))
|
|
(t (funcall tacc-play-sound-file tacc-sound-tick))))))
|
|
|
|
(defun tacc-timer-buffer-redraw ()
|
|
(let ((now (current-time))
|
|
(state (alist-get 'state tacc-info)))
|
|
(tacc-timer-buffer-update state)))
|
|
;; TODO
|
|
|
|
(defun tacc-schedule ()
|
|
"Note the time and schedule the first tick"
|
|
(setcdr (assoc 'state tacc-info) 'work)
|
|
(setcdr (assoc 'work-ts tacc-info) (current-time))
|
|
(setq tacc-timer (run-with-timer 't 1 'tacc-tick)))
|
|
|
|
(defun tacc-deschedule ()
|
|
"Deschedule the timer"
|
|
(setcdr (assoc 'end-ts tacc-info) (current-time))
|
|
(setq tacc-timer (cancel-timer tacc-timer)))
|
|
|
|
(defun tacc-push-current ()
|
|
"Push the current period into the record"
|
|
(let* ((stack (assoc 'history tacc-info))
|
|
(work-place (assoc 'work-ts tacc-info))
|
|
(break-place (assoc 'break-ts tacc-info))
|
|
(end-place (assoc 'end-ts tacc-info))
|
|
(tag (alist-get 'tag tacc-info "")))
|
|
(let ((work-cond (if (cdr work-place) 't nil))
|
|
(break-cond (if (cdr break-place) 't nil)))
|
|
(if work-cond
|
|
(let ((ts (time-to-seconds (cdr work-place)))
|
|
(end (time-to-seconds (if break-cond
|
|
(cdr break-place)
|
|
(cdr end-place)))))
|
|
(push `((ts . ,ts)
|
|
(end . ,end)
|
|
(state . work)
|
|
(tag . ,tag))
|
|
stack)))
|
|
(if break-cond
|
|
(let ((ts (time-to-seconds (cdr break-place)))
|
|
(end (time-to-seconds (cdr end-place))))
|
|
(push `((ts . ,ts)
|
|
(end . ,end)
|
|
(state . break)
|
|
(tag . ,tag))
|
|
stack))))
|
|
(setcdr work-place nil)
|
|
(setcdr break-place nil)
|
|
(setcdr end-place nil)))
|
|
|
|
(defun tacc-kill-buffer-confirm ()
|
|
"Ask for confirmation if a timer is still running"
|
|
(if (tacc-running?)
|
|
(yes-or-no-p "Are you sure you want to kill the timer buffer? This will\
|
|
end the current timer session as well.")
|
|
t))
|
|
|
|
(defun tacc-buffer-cleanup ()
|
|
"Save and cleanup the timer state if a timer is still live"
|
|
(if (tacc-running?)
|
|
(progn (tacc-deschedule)
|
|
(tacc-push-current))))
|
|
|
|
;; 06.4 Rendering
|
|
|
|
(defun space-divider (width segments)
|
|
(let ((ticker 0)
|
|
(rem (mod width segments))
|
|
(len (/ width segments)))
|
|
(lambda ()
|
|
(setf ticker (1+ ticker))
|
|
(+ len (if (<= ticker rem) 1 0)))))
|
|
|
|
(defun gradient-string (width chars)
|
|
(mapconcat (let ((ticker 0)
|
|
(rem (mod width (length chars)))
|
|
(len (/ width (length chars))))
|
|
(lambda (c)
|
|
(make-string (+ len (if (<= (setq ticker (1+ ticker)) rem) 1 0))
|
|
c)))
|
|
chars ""))
|
|
|
|
(defun tacc-header-render (width)
|
|
"Returns a string for the header of the tacc timer"
|
|
(propertize (gradient-string width "'\"'") ;▓░ ▒█
|
|
'face 'shadow))
|
|
|
|
(defmacro tacc-render-with-buffer (&rest body)
|
|
"Executes the body in tacc-buffer with (let-alist tacc-info), suppress read only"
|
|
`(let* ((buffer (tacc-buffer))
|
|
(window (get-buffer-window buffer t)))
|
|
(with-current-buffer buffer
|
|
(let ((width (window-total-width window 'floor))
|
|
(inhibit-read-only t))
|
|
(let-alist (buffer-local-value 'tacc-locs buffer)
|
|
(goto-char (point-min))
|
|
,@body)))))
|
|
|
|
(defmacro tacc-insert-between (start end &rest strings)
|
|
"Replace the text in the region between start and end with string"
|
|
`(save-restriction
|
|
(narrow-to-region ,start ,end)
|
|
(delete-region ,start ,end)
|
|
(goto-char ,start)
|
|
(insert ,@strings)
|
|
(set-marker ,start (point-min))
|
|
(set-marker ,end (point-max))))
|
|
|
|
(defconst tacc-pause-glyph ?⏸)
|
|
(defconst tacc-run-glyph ?▶)
|
|
(defconst tacc-graph-char ?█)
|
|
|
|
(defun tacc-init-timer-buffer ()
|
|
"Initialized the timer buffer display. Sets needed edit positions"
|
|
(let* ((buffer (get-buffer-create tacc-buffer-name))
|
|
(window (display-buffer buffer
|
|
(append tacc-display-buffer-settings
|
|
tacc-display-buffer-window-settings))))
|
|
(with-current-buffer buffer
|
|
(tacc-timer-mode)
|
|
(make-local-variable 'kill-buffer-query-functions)
|
|
(add-hook 'kill-buffer-query-functions 'tacc-kill-buffer-confirm)
|
|
(let ((width (window-total-width window 'floor))
|
|
(inhibit-read-only t))
|
|
(goto-char (point-min))
|
|
(let-alist (buffer-local-value 'tacc-locs buffer)
|
|
(insert (tacc-header-render width))
|
|
(set-marker .clock-start (point))
|
|
(set-marker .clock-end (point-max))
|
|
(tacc-insert-between
|
|
.clock-start .clock-end
|
|
(tacc-clock-render 'work 1 0))
|
|
(insert "\n")
|
|
(set-marker .graph-start (point))
|
|
(set-marker .graph-end (point-max))
|
|
(tacc-insert-between
|
|
.graph-start .graph-end
|
|
"Tag: \n"
|
|
(tacc-graph-bar-render width 0.0 0.0 1.0)))))))
|
|
|
|
|
|
(defun tacc-clock-glyph (state)
|
|
"Returns the correct play or pause glyph for each state"
|
|
(string
|
|
(cond ((eq state 'work) tacc-run-glyph)
|
|
((eq state 'break) tacc-run-glyph)
|
|
((eq state 'pause) tacc-pause-glyph)
|
|
((eq state 'stop) tacc-pause-glyph))))
|
|
|
|
(defun tacc-clock-render (state countdown now)
|
|
"Draw the clock"
|
|
(let* ((overwork (time-less-p countdown 1))
|
|
(count-fmt (if overwork "+%M:%S" "-%M:%S"))
|
|
(count-face (cond (overwork 'tacc-overwork)
|
|
((eq state 'pause) 'tacc-clock-pause)
|
|
('t 'tacc-clock))))
|
|
(concat
|
|
(propertize (format-time-string "%T" now) 'face 'tacc-clock)
|
|
(propertize (format-time-string count-fmt countdown) 'face count-face)
|
|
(propertize (tacc-clock-glyph state) 'face 'tacc-clock))))
|
|
|
|
(defun tacc-graph-bar-render (width work break void)
|
|
"Return a graph bar width characters wide with the given proportions as \
|
|
float"
|
|
(let ((width (- width 5)))
|
|
(concat
|
|
" "
|
|
(propertize (make-string (round (* width work)) tacc-graph-char)
|
|
'face 'tacc-graph-work)
|
|
(propertize (make-string (round (* width break)) tacc-graph-char)
|
|
'face 'tacc-graph-break)
|
|
(propertize (make-string (round (* width void)) tacc-graph-char)
|
|
'face 'tacc-graph-void)
|
|
" \n")))
|
|
|
|
(defun tacc-tag-render (tag)
|
|
(concat " Tag: "
|
|
(propertize (if (string= tag "") "<None>" tag)
|
|
'face 'tacc-tag)
|
|
"\n"))
|
|
|
|
(defun tacc-timer-buffer-update (state est-ts countdown now)
|
|
"Update the timer buffer rendering"
|
|
(tacc-render-with-buffer
|
|
(tacc-insert-between
|
|
.clock-start .clock-end
|
|
(let-alist tacc-info
|
|
(tacc-clock-render state countdown now)))
|
|
(tacc-insert-between
|
|
.graph-start .graph-end
|
|
(let-alist tacc-info
|
|
(concat (tacc-tag-render .tag)
|
|
(let* ((est-s (time-to-seconds est-ts))
|
|
(now-s (time-to-seconds now))
|
|
(end-s (if (< est-s now-s) now-s est-s))
|
|
(work-s (time-to-seconds .work-ts))
|
|
(break-s (time-to-seconds .break-ts))
|
|
|
|
(total (- end-s work-s))
|
|
(work-prop (/ (- (if (numberp break-s) break-s now-s) work-s) total))
|
|
(break-prop (/ (if (numberp break-s) (- now-s break-s) 0) total))
|
|
(void-prop (- 1 (+ work-prop break-prop))))
|
|
(tacc-graph-bar-render width work-prop break-prop void-prop)))))))
|
|
|
|
(defun tacc-new-graph-bar-render ()
|
|
(tacc-render-with-buffer
|
|
(save-restriction
|
|
(narrow-to-region .graph-start .graph-start)
|
|
(goto-char (point-min))
|
|
(insert "\n")
|
|
(set-marker .graph-start (point-min))
|
|
(set-marker .graph-end (point-max))
|
|
(insert "\n"))))
|
|
|
|
;; 07 Init
|
|
|
|
(defun tacc-init ()
|
|
"Orchestrates all initialization functions"
|
|
(let ((tag (tacc-tag-prompt)))
|
|
(tacc-init-timer-buffer)
|
|
(setcdr (assoc 'tag tacc-info) tag)
|
|
(tacc-schedule)))
|
|
|
|
;; 08.1 Interactive Helpers
|
|
|
|
(defun tacc-tag-prompt ()
|
|
"Prompt for a tag and return the user's input"
|
|
(let ((last-tag (if (bufferp (tacc-buffer))
|
|
(alist-get 'tag tacc-info)
|
|
nil))
|
|
(completions (append (flatten-tree
|
|
(apply 'funcall (if tacc-tag-completion-functions
|
|
tacc-tag-completion-functions
|
|
`(,(lambda () "")))))
|
|
tacc-tag-history)))
|
|
(completing-read (concat "Enter a tag name"
|
|
(if (< (length last-tag) 0)
|
|
(format " (currently %s)" last-tag) "")
|
|
": ")
|
|
completions
|
|
'stringp
|
|
nil
|
|
nil
|
|
'tacc-tag-history)))
|
|
|
|
(defun tacc-tag-prompt-soft ()
|
|
"Prompt for a tag unless the user doesn't want us to
|
|
Returns the current tag if no prompt is made"
|
|
(if tacc-suppress-tag-prompt
|
|
(alist-get 'tag tacc-info)
|
|
(tacc-tag-prompt)))
|
|
|
|
;; 08.2 Interactive
|
|
|
|
(defun tacc-start-timer ()
|
|
"Starts the tacc timer"
|
|
(interactive)
|
|
(if (bufferp (tacc-buffer))
|
|
(message "A timer is already running")
|
|
(tacc-init)))
|
|
|
|
(defun tacc-set-tag ()
|
|
"Sets a tag for live timer sessions going forward"
|
|
(interactive)
|
|
(if (tacc-running?)
|
|
(setcdr (assoc 'tag tacc-info) (tacc-tag-prompt))
|
|
(message "There isn't a live timer session to tag")))
|
|
|
|
(defun tacc-cycle-increment (state)
|
|
"Increments the cycle counter if the current state is a 'break"
|
|
(if (eq state 'break)
|
|
(let* ((cycle-place (assoc 'cycle tacc-info))
|
|
(cycle (cdr cycle-place)))
|
|
(setcdr cycle-place (1+ cycle)))))
|
|
|
|
(defun tacc-timer-state-forward ()
|
|
"Move the tacc timer state forward"
|
|
(interactive)
|
|
(if (tacc-running?)
|
|
(let* ((state-place (assoc 'state tacc-info))
|
|
(state (cdr state-place))
|
|
(work-ts (assoc 'work-ts tacc-info))
|
|
(break-ts (assoc 'break-ts tacc-info))
|
|
(end-ts (assoc 'end-ts tacc-info))
|
|
(ts (current-time)))
|
|
(if (eq state 'work)
|
|
(progn (setcdr break-ts ts)
|
|
(setcdr state-place 'break))
|
|
(progn (setcdr end-ts ts)
|
|
(tacc-push-current)
|
|
(setcdr work-ts ts)
|
|
(setcdr state-place 'work)
|
|
(tacc-new-graph-bar-render)))
|
|
(tacc-cycle-increment state))
|
|
(message "There is no timer to advance")))
|
|
|
|
(defun tacc-timer-state-skip ()
|
|
"Move the tacc timer state to a new work cycle"
|
|
(interactive)
|
|
(if (tacc-running?)
|
|
(let* ((state-place (assoc 'state tacc-info))
|
|
(work-ts (assoc 'work-ts tacc-info))
|
|
(end-ts (assoc 'end-ts tacc-info))
|
|
(ts (current-time)))
|
|
(setcdr end-ts ts)
|
|
(tacc-push-current)
|
|
(tacc-cycle-increment 'break)
|
|
(setcdr work-ts ts)
|
|
(setcdr state-place 'work)
|
|
(tacc-new-graph-bar-render))))
|
|
|
|
(defun tacc-pause-timer ()
|
|
"Pause the tacc timer session"
|
|
(tacc-deschedule)
|
|
(tacc-push-current)
|
|
(setcdr (assoc 'state tacc-info) 'pause))
|
|
|
|
(defun tacc-unpause-timer ()
|
|
"Unpause the tacc timer session"
|
|
(tacc-new-graph-bar-render)
|
|
(tacc-schedule))
|
|
|
|
(defun tacc-hold-timer ()
|
|
"Pause/unpause the tacc timer session"
|
|
(interactive)
|
|
(if (tacc-running?)
|
|
(let ((state (alist-get 'state tacc-info)))
|
|
(if (eq state 'pause)
|
|
(tacc-unpause-timer)
|
|
(tacc-pause-timer))
|
|
(tacc-timer-buffer-redraw)))
|
|
(message "There isn't a live timer session to pause/unpause"))
|
|
|
|
(defun tacc-end-timer ()
|
|
"Ends the current timer session, runs serialization and kills the buffer"
|
|
(interactive)
|
|
(tacc-deschedule)
|
|
(tacc-push-current)
|
|
(tacc-save-record (assoc 'history tacc-info))
|
|
(setcdr (assoc 'state tacc-info) 'stop)
|
|
(kill-buffer (tacc-buffer)))
|
|
|
|
;; 09 Hooks setup
|
|
(add-hook 'after-init-hook 'tacc-load-tag-history)
|
|
(add-hook 'kill-emacs-hook 'tacc-save-tag-history)
|
|
|
|
;; 10 Provides
|
|
|
|
(provide 'tacc)
|
|
|
|
;;; tacc.el ends here
|