2018-08-27 07:56:04 +00:00
|
|
|
(require 'timeclock)
|
|
|
|
(require 'dash)
|
2018-08-27 20:22:06 +00:00
|
|
|
(require 'cl-lib)
|
2018-09-01 12:31:25 +00:00
|
|
|
(require 'timeclock-report)
|
2018-08-27 07:56:04 +00:00
|
|
|
|
|
|
|
;; 2018-08-27T12:45:03+0530
|
2018-08-27 20:22:06 +00:00
|
|
|
|
2018-08-28 03:47:35 +00:00
|
|
|
;; TODO
|
2018-09-11 12:28:25 +00:00
|
|
|
;; 1. Refresh when you select the list buffer (impossible? make-thread
|
2018-08-28 03:01:31 +00:00
|
|
|
;; in v26? Use emacs-async library?)
|
|
|
|
;; 2. Add support for prefix args to tclist/toggle-project
|
|
|
|
;; 3. Add variable to let user control prompting-for-reason behaviour
|
2018-09-11 12:42:24 +00:00
|
|
|
;; 4. Option to use a specific time to define when a day starts/ends.
|
2018-08-27 20:22:06 +00:00
|
|
|
;; e.g. 08:00 will mean a day starts and ends at 08:00 instead of
|
2018-08-28 18:17:25 +00:00
|
|
|
;; the usual 24:00/00:00. Helpful for late sleepers.
|
2018-09-02 09:24:39 +00:00
|
|
|
;; 5. Give each line a number - press the number to clock in/out
|
2018-09-01 12:31:25 +00:00
|
|
|
;; - shortcuts derived from the first alphabet of each project
|
|
|
|
;; could be even nicer, but the code to generate them from
|
|
|
|
;; similarly-named projects would be somewhat complex
|
2018-09-11 12:42:24 +00:00
|
|
|
;; 6. Make clocked-in project row bold, either in addition to the
|
2018-08-28 18:17:25 +00:00
|
|
|
;; star, or replacing it.
|
2018-09-11 12:42:24 +00:00
|
|
|
;; 7. Show currently active project + time spent on it so far in the
|
2018-09-01 13:49:29 +00:00
|
|
|
;; mode-line (see timeclock-mode-line-display)
|
2018-09-11 12:42:24 +00:00
|
|
|
;; 8. The default reason suggested is the last one used. Can't even
|
2018-09-01 13:49:29 +00:00
|
|
|
;; begin to explain how nonsensical that is. (might be an ido
|
|
|
|
;; problem)
|
2018-09-11 12:42:24 +00:00
|
|
|
;; 9. Show shortcuts message by using the keymap rather than a
|
|
|
|
;; hardcoded string.
|
2018-08-31 14:37:52 +00:00
|
|
|
|
2018-08-28 03:47:35 +00:00
|
|
|
;; BUGS
|
2018-09-01 20:46:05 +00:00
|
|
|
;; 1. (goto-char (point-max)) -> RET -> the time spent on the last
|
2018-08-31 14:37:52 +00:00
|
|
|
;; project in the list will be the first new project suggestion.
|
2018-08-28 03:47:35 +00:00
|
|
|
|
|
|
|
;; Style issues
|
2018-09-01 12:31:25 +00:00
|
|
|
;; 1. Uses Scheme-style ? and x->y naming conventions instead of
|
2018-08-28 18:17:25 +00:00
|
|
|
;; Elisp/CL-style "-p" and "x-to-y"
|
2018-09-01 12:31:25 +00:00
|
|
|
;; - ido uses ? for 'completion help', so you can't type ? unless
|
|
|
|
;; you unset that o\
|
2018-08-28 18:17:25 +00:00
|
|
|
;; 2. Should use *earmuffs* for global variables for clarity
|
2018-09-01 12:31:25 +00:00
|
|
|
;; 3. Should names of major modes (timeclock-list-mode,
|
|
|
|
;; timeclock-report-mode) end with -major-mode ?
|
2018-08-27 20:22:06 +00:00
|
|
|
|
|
|
|
;; Limitations of timeclock.el
|
|
|
|
;; 1. Concurrent tasks not permitted
|
2018-08-28 18:17:25 +00:00
|
|
|
;; 2. timeclock-project-list contains only the projects found in the
|
|
|
|
;; timeclock-file - no way for a user to specify tasks beforehand.
|
2018-09-01 12:31:25 +00:00
|
|
|
;; 3. Uses non-standard slashes in the date instead of dashes (e.g.
|
|
|
|
;; "2018/01/01" instead of "2018-01-01") and a space for the
|
|
|
|
;; date-time separator instead of T
|
2018-09-01 16:19:47 +00:00
|
|
|
;;
|
|
|
|
;; Limitations of tabulated-list-mode
|
|
|
|
;; 1. Can't mix tabulated and non-tabulated data!!! What if I want
|
|
|
|
;; some buttons, separate from the data but part of the same
|
|
|
|
;; buffer?!
|
2018-09-11 12:28:25 +00:00
|
|
|
|
2018-09-01 12:31:25 +00:00
|
|
|
;; ## IDLE TIMER ##
|
2018-09-11 12:41:00 +00:00
|
|
|
(defun tcl/buffer-exists? (buffer-name)
|
2018-08-28 03:48:31 +00:00
|
|
|
(--> (buffer-list)
|
|
|
|
(mapcar #'buffer-name it)
|
|
|
|
(member buffer-name it)))
|
|
|
|
|
2018-09-11 12:41:00 +00:00
|
|
|
(defun tcl/buffer-visible? (buffer-or-buffer-name)
|
2018-09-11 12:28:25 +00:00
|
|
|
"Returns t if BUFFER-OR-BUFFER-NAME is visible to user."
|
2018-08-28 18:17:25 +00:00
|
|
|
;; It'd be simpler to use only the windows of the current frame (-->
|
|
|
|
;; (selected-frame) (window-list it) ...) - but it wouldn't be
|
|
|
|
;; robust, because it is possible that a frame partially covers
|
|
|
|
;; another and the buffer is visible to the user from the latter.
|
2018-09-11 12:28:25 +00:00
|
|
|
(-->
|
|
|
|
(visible-frame-list)
|
|
|
|
(mapcar #'window-list it)
|
|
|
|
(mapcar (lambda (list)
|
|
|
|
(mapcar #'window-buffer list))
|
|
|
|
it)
|
|
|
|
(mapcar (lambda (list)
|
|
|
|
(mapcar (lambda (buffer)
|
|
|
|
(if (bufferp buffer-or-buffer-name)
|
|
|
|
(equal buffer-or-buffer-name buffer)
|
|
|
|
(equal (buffer-name buffer)
|
|
|
|
buffer-or-buffer-name)))
|
|
|
|
list))
|
|
|
|
it)
|
|
|
|
(mapcar (lambda (list)
|
|
|
|
(seq-filter #'identity list))
|
|
|
|
it)
|
2018-09-01 12:31:25 +00:00
|
|
|
(mapcar #'car it)
|
|
|
|
(if (car it) t nil)))
|
2018-09-11 12:28:25 +00:00
|
|
|
|
2018-09-11 12:41:00 +00:00
|
|
|
(defun tcl/timer-fn ()
|
|
|
|
(when (and (tcl/buffer-exists? timeclock-list-buffer-name)
|
|
|
|
(tcl/buffer-visible? timeclock-list-buffer-name))
|
2018-09-11 12:28:25 +00:00
|
|
|
(with-current-buffer timeclock-list-buffer-name
|
2018-09-01 20:46:05 +00:00
|
|
|
(tabulated-list-print t))))
|
2018-08-27 09:14:57 +00:00
|
|
|
|
2018-09-01 12:31:25 +00:00
|
|
|
;; ## VARIABLES ##
|
2018-08-27 09:14:57 +00:00
|
|
|
(defvar time-re "[0-9]\\{2\\}:[0-9]\\{2\\}:[0-9]\\{2\\}")
|
2018-09-01 20:35:18 +00:00
|
|
|
(defvar empty-time-string "-")
|
|
|
|
(defvar time-re-list (rx-to-string
|
|
|
|
`(or
|
|
|
|
(and (optional (repeat 1 2 digit) ":")
|
|
|
|
(and (repeat 1 2 digit) ":" (repeat 2 digit)))
|
|
|
|
,empty-time-string)))
|
2018-09-11 12:27:34 +00:00
|
|
|
(defvar timeclock-list-buffer-name "*Timeclock-List*")
|
2018-08-27 09:14:57 +00:00
|
|
|
|
2018-09-01 12:31:25 +00:00
|
|
|
;; ## FUNCTIONS ##
|
2018-09-01 16:19:47 +00:00
|
|
|
|
|
|
|
;; tests -
|
|
|
|
;; (mapcar #'tcr/format-time
|
|
|
|
;; '((0 0 0) (0 0 1) (0 0 10) (0 1 10) (0 10 10) (1 10 10) (10 10 10)))
|
|
|
|
;; => ("" "00:01" "00:10" "01:10" "10:10" "01:10:10" "10:10:10")
|
|
|
|
;; (mapcar #'tcr/format-time
|
|
|
|
;; '([0 0 0] [0 0 1] [0 0 10] [0 1 10] [0 10 10] [1 10 10] [10 10 10]))
|
|
|
|
;; => ("" "00:01" "00:10" "01:10" "10:10" "01:10:10" "10:10:10")
|
|
|
|
(defun tcl/format-time (time)
|
|
|
|
"Formats and displays TIME, where time is a vector or a list of
|
|
|
|
the form [HOURS MINUTES SECONDS] or (HOURS MINUTES SECONDS)."
|
|
|
|
(let ((h (elt time 0))
|
|
|
|
(m (elt time 1))
|
|
|
|
(s (elt time 2)))
|
|
|
|
(if (and (zerop h) (zerop m) (zerop s))
|
|
|
|
"-"
|
|
|
|
(let ((h (if (zerop h)
|
|
|
|
""
|
|
|
|
(format "%02d:" h))) ;; can't change this just yet or all commands break
|
|
|
|
(m (format "%02d:" m))
|
|
|
|
(s (format "%02d" s)))
|
|
|
|
(concat h m s)))))
|
|
|
|
|
2018-09-11 12:41:00 +00:00
|
|
|
(defun tcl/current-project ()
|
2018-08-27 09:14:57 +00:00
|
|
|
"Returns the name of the currently clocked-in project, or nil
|
|
|
|
if the user is not clocked in."
|
|
|
|
(if (not (timeclock-currently-in-p))
|
|
|
|
nil
|
|
|
|
(with-current-buffer (find-file-noselect timeclock-file)
|
2018-08-31 14:29:50 +00:00
|
|
|
(save-excursion
|
|
|
|
(goto-char (point-max))
|
|
|
|
(forward-line -1)
|
|
|
|
(re-search-forward (concat time-re " ") nil t)
|
|
|
|
(buffer-substring-no-properties (point) (point-at-eol))))))
|
2018-08-27 09:14:57 +00:00
|
|
|
|
2018-09-11 12:41:00 +00:00
|
|
|
(defun tcl/project-active? (project)
|
2018-08-27 09:14:57 +00:00
|
|
|
"Returns t if PROJECT is currently clocked in, else nil."
|
2018-09-11 12:41:00 +00:00
|
|
|
(equal (tcl/current-project) project))
|
2018-08-27 07:56:04 +00:00
|
|
|
|
2018-09-11 12:41:00 +00:00
|
|
|
(defun tcl/timestamp->seconds (date-time)
|
2018-08-27 07:56:04 +00:00
|
|
|
"Converts a timestamp to seconds since 00:00"
|
|
|
|
(--> date-time
|
|
|
|
(split-string it "[/ :]")
|
|
|
|
(mapcar #'string-to-number it)
|
|
|
|
(reverse it)
|
|
|
|
(apply #'encode-time it)))
|
|
|
|
|
|
|
|
;; tests -
|
2018-09-11 12:41:00 +00:00
|
|
|
;; (mapcar #'tcl/seconds-to-hms '(1 60 61 3600 3660 3661))
|
2018-08-27 07:58:35 +00:00
|
|
|
;; => ([0 0 1] [0 1 0] [0 1 1] [1 0 0] [1 1 0] [1 1 1])
|
2018-09-11 12:41:00 +00:00
|
|
|
(defun tcl/seconds-to-hms (seconds)
|
2018-08-27 07:58:35 +00:00
|
|
|
(setq seconds (truncate seconds))
|
|
|
|
(let* ((s (% seconds 60))
|
|
|
|
(m (% (/ seconds 60) 60))
|
|
|
|
(h (/ seconds 3600)))
|
|
|
|
(vector h m s)))
|
2018-08-27 07:56:04 +00:00
|
|
|
|
|
|
|
;; The multiple calls to re-search-forward/backward to get point at
|
2018-09-01 12:31:25 +00:00
|
|
|
;; the right spot are just ugly :\
|
|
|
|
;;
|
2018-08-28 18:17:25 +00:00
|
|
|
;; Could be refactored - one function to get ranges for an activity,
|
|
|
|
;; one to convert them to seconds, one to subtract them (get an
|
|
|
|
;; interval from two timestamps), and one to output the time vector
|
2018-09-11 12:41:00 +00:00
|
|
|
;; from tcl/seconds-to-hms in the desired format
|
|
|
|
(defun tcl/project-time-one-day (project &optional date)
|
2018-09-01 12:31:25 +00:00
|
|
|
"Read `timeclock-file' and return total time spent on a project
|
|
|
|
in one day. If DATE is a string in the form \"YYYY-MM-DD\", the
|
|
|
|
time for that date is shown, otherwise calculate time for that
|
|
|
|
day."
|
2018-08-27 07:56:04 +00:00
|
|
|
(if (not (member project timeclock-project-list))
|
|
|
|
(error (concat "Unknown project: " project))
|
2018-09-01 12:31:25 +00:00
|
|
|
(let* ((target-date (if date
|
|
|
|
(replace-regexp-in-string "-" "/" date) ;; should probably validate it...
|
|
|
|
(format-time-string "%Y/%m/%d")))
|
|
|
|
(search-re (concat target-date " " time-re " " project))
|
2018-08-27 07:56:04 +00:00
|
|
|
(interval-list nil))
|
|
|
|
(with-current-buffer (find-file-noselect timeclock-file)
|
2018-08-31 14:29:50 +00:00
|
|
|
(save-excursion
|
|
|
|
(goto-char (point-min))
|
|
|
|
(while (re-search-forward (concat "i " search-re) nil t)
|
2018-09-01 12:31:25 +00:00
|
|
|
(re-search-backward target-date nil t)
|
2018-08-31 14:29:50 +00:00
|
|
|
(let* ((start-time (buffer-substring-no-properties
|
|
|
|
(point)
|
|
|
|
(+ 10 1 8 (point))))
|
|
|
|
(end-time (progn
|
2018-09-01 12:31:25 +00:00
|
|
|
(if (re-search-forward (concat "o " target-date) nil t)
|
2018-08-31 14:29:50 +00:00
|
|
|
(buffer-substring-no-properties (- (point) 10)
|
|
|
|
(+ 9 (point)))
|
|
|
|
;; if the user hasn't clocked out
|
|
|
|
;; from the project, the timelog does
|
|
|
|
;; not have an ending time yet, so we
|
|
|
|
;; use the current time
|
|
|
|
(format-time-string "%Y/%m/%d %T"))))
|
|
|
|
(interval (-->
|
2018-09-11 12:41:00 +00:00
|
|
|
(time-subtract (tcl/timestamp->seconds end-time)
|
|
|
|
(tcl/timestamp->seconds start-time))
|
2018-08-31 14:29:50 +00:00
|
|
|
(elt it 1))))
|
|
|
|
(setq interval-list
|
|
|
|
(append interval-list (list interval)))))
|
2018-09-01 16:19:47 +00:00
|
|
|
(->>
|
|
|
|
(seq-reduce #'+ interval-list 0)
|
|
|
|
(tcl/seconds-to-hms)
|
|
|
|
(tcl/format-time)))))))
|
2018-08-27 07:56:04 +00:00
|
|
|
|
2018-09-11 12:41:00 +00:00
|
|
|
(defun tcl/entries ()
|
2018-09-11 12:39:38 +00:00
|
|
|
"Creates entries to be displayed in the buffer created by
|
|
|
|
`timeclock-list'."
|
2018-09-01 20:46:05 +00:00
|
|
|
(timeclock-reread-log)
|
2018-09-11 12:39:38 +00:00
|
|
|
(mapcar (lambda (project-name)
|
|
|
|
(list project-name
|
|
|
|
(vector project-name
|
2018-09-11 12:41:00 +00:00
|
|
|
(tcl/project-time-one-day project-name)
|
|
|
|
(if (tcl/project-active? project-name)
|
2018-09-11 12:39:38 +00:00
|
|
|
"*" ""))))
|
|
|
|
timeclock-project-list))
|
2018-09-11 12:27:34 +00:00
|
|
|
|
2018-09-02 05:45:46 +00:00
|
|
|
(defun tcl/project-at-point ()
|
|
|
|
(beginning-of-line)
|
|
|
|
(--> (buffer-substring-no-properties
|
|
|
|
(point)
|
|
|
|
(progn
|
|
|
|
(re-search-forward time-re-list nil t)
|
|
|
|
(match-beginning 0)))
|
|
|
|
(replace-regexp-in-string "[ \t]*$" "" it)) )
|
|
|
|
|
2018-09-01 12:31:25 +00:00
|
|
|
;; ## MAJOR-MODE ##
|
2018-09-11 12:39:38 +00:00
|
|
|
(define-derived-mode timeclock-list-mode tabulated-list-mode "Timeclock-List"
|
2018-09-01 12:31:25 +00:00
|
|
|
"Major mode for `timeclock-list'."
|
2018-09-11 12:39:38 +00:00
|
|
|
(timeclock-reread-log)
|
|
|
|
|
|
|
|
(make-local-variable 'tabulated-list-format)
|
|
|
|
(setq tabulated-list-format [("Project" 25 t) ("Time" 10 t) ("Active" 3 t)])
|
|
|
|
|
|
|
|
(make-local-variable 'tabulated-list-entries)
|
2018-09-11 12:41:00 +00:00
|
|
|
(setq tabulated-list-entries 'tcl/entries)
|
2018-09-11 12:39:38 +00:00
|
|
|
|
|
|
|
(make-local-variable 'tabulated-list-sort-key)
|
|
|
|
(setq tabulated-list-sort-key '("Project" . nil))
|
|
|
|
|
|
|
|
(tabulated-list-init-header)
|
|
|
|
|
2018-09-11 12:41:00 +00:00
|
|
|
(run-with-idle-timer 3 t #'tcl/timer-fn)
|
|
|
|
(define-key timeclock-list-mode-map (kbd "RET") 'tcl/toggle-project)
|
2018-09-11 12:42:24 +00:00
|
|
|
(define-key timeclock-list-mode-map (kbd "l") 'tcl/open-timeclock-file)
|
|
|
|
(define-key timeclock-list-mode-map (kbd "r") 'timeclock-report))
|
2018-09-11 12:39:38 +00:00
|
|
|
|
2018-09-01 12:31:25 +00:00
|
|
|
;; ## COMMANDS ##
|
|
|
|
|
2018-09-11 12:41:00 +00:00
|
|
|
(defun tcl/toggle-project ()
|
2018-09-11 12:39:38 +00:00
|
|
|
"In a `timeclock-list' buffer, start or stop the project at point."
|
|
|
|
(interactive)
|
2018-09-02 05:45:46 +00:00
|
|
|
(let ((project-at-point (tcl/project-at-point))
|
2018-09-11 12:41:00 +00:00
|
|
|
(current-project (tcl/current-project)))
|
2018-09-11 12:39:38 +00:00
|
|
|
;; When changing projects/clocking in, suggest the project at point
|
|
|
|
(cl-letf (((symbol-function 'timeclock-ask-for-project)
|
|
|
|
(lambda ()
|
|
|
|
(timeclock-completing-read
|
|
|
|
(format "Clock into which project (default %s): "
|
|
|
|
project-at-point)
|
|
|
|
(mapcar 'list timeclock-project-list)
|
|
|
|
project-at-point))))
|
|
|
|
;; If we're clocked in to anything - clock out or change projects
|
|
|
|
(if current-project
|
|
|
|
(if (equal project-at-point current-project)
|
|
|
|
(timeclock-out nil nil t)
|
|
|
|
;; We don't use timeclock-change because it doesn't prompt for the reason
|
|
|
|
(progn
|
|
|
|
(timeclock-out nil nil t)
|
|
|
|
(timeclock-in nil nil t)))
|
|
|
|
;; Otherwise, run timeclock-in with project at point as default
|
|
|
|
;; suggestion
|
|
|
|
(timeclock-in nil nil t)))
|
|
|
|
(timeclock-reread-log) ;; required when we create a new activity
|
|
|
|
;; Trying to update partially doesn't update the activity indicator. Why?
|
|
|
|
(tabulated-list-print t nil)))
|
2018-09-11 12:27:34 +00:00
|
|
|
|
2018-09-11 12:41:00 +00:00
|
|
|
(defun tcl/open-timeclock-file ()
|
2018-09-11 12:39:38 +00:00
|
|
|
(interactive)
|
|
|
|
(find-file-other-window timeclock-file)
|
|
|
|
(goto-char (point-max)))
|
|
|
|
|
2018-09-01 16:19:47 +00:00
|
|
|
(defun timeclock-list (&optional arg)
|
2018-09-11 12:39:38 +00:00
|
|
|
"Displays a list of the user's timeclock.el projects and the
|
|
|
|
time spent on each today, based on their timelog file
|
|
|
|
`timeclock-file'. The user can hit RET to start/stop projects.
|
|
|
|
This is the 'listing command' for timeclock-list-mode."
|
2018-09-01 16:19:47 +00:00
|
|
|
(interactive "P")
|
|
|
|
(if arg
|
|
|
|
(timeclock-report)
|
|
|
|
(let ((buffer (get-buffer-create timeclock-list-buffer-name)))
|
|
|
|
(if (tcl/buffer-visible? timeclock-list-buffer-name)
|
|
|
|
(kill-buffer timeclock-list-buffer-name)
|
|
|
|
(with-current-buffer buffer
|
|
|
|
(timeclock-list-mode)
|
|
|
|
(tabulated-list-print)
|
2018-09-02 09:24:39 +00:00
|
|
|
|
|
|
|
;; place point on the last or current project
|
|
|
|
(goto-char (point-min))
|
|
|
|
(re-search-forward timeclock-last-project nil t)
|
|
|
|
(beginning-of-line)
|
|
|
|
|
2018-09-11 12:42:24 +00:00
|
|
|
(switch-to-buffer buffer)
|
|
|
|
(message "RET - clock in/out, r - see weekly report, l - open log file"))))))
|
2018-09-11 12:27:34 +00:00
|
|
|
|
2018-08-27 07:56:04 +00:00
|
|
|
(provide 'timeclock-list)
|