| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
|
|---|
| 5 |
|
|---|
| 6 |
|
|---|
| 7 |
|
|---|
| 8 |
|
|---|
| 9 |
|
|---|
| 10 |
|
|---|
| 11 |
|
|---|
| 12 |
|
|---|
| 13 |
|
|---|
| 14 |
|
|---|
| 15 |
|
|---|
| 16 |
|
|---|
| 17 |
|
|---|
| 18 |
|
|---|
| 19 |
|
|---|
| 20 |
|
|---|
| 21 |
|
|---|
| 22 |
|
|---|
| 23 |
|
|---|
| 24 |
|
|---|
| 25 |
|
|---|
| 26 |
|
|---|
| 27 |
|
|---|
| 28 |
|
|---|
| 29 |
|
|---|
| 30 |
|
|---|
| 31 |
|
|---|
| 32 |
|
|---|
| 33 |
|
|---|
| 34 |
|
|---|
| 35 |
|
|---|
| 36 |
|
|---|
| 37 |
|
|---|
| 38 |
|
|---|
| 39 |
|
|---|
| 40 |
|
|---|
| 41 |
|
|---|
| 42 |
|
|---|
| 43 |
|
|---|
| 44 |
|
|---|
| 45 |
|
|---|
| 46 |
|
|---|
| 47 |
|
|---|
| 48 |
|
|---|
| 49 |
|
|---|
| 50 |
|
|---|
| 51 |
|
|---|
| 52 |
|
|---|
| 53 |
|
|---|
| 54 |
|
|---|
| 55 |
|
|---|
| 56 |
|
|---|
| 57 |
|
|---|
| 58 |
|
|---|
| 59 |
|
|---|
| 60 |
|
|---|
| 61 |
|
|---|
| 62 |
|
|---|
| 63 |
|
|---|
| 64 |
|
|---|
| 65 |
|
|---|
| 66 |
|
|---|
| 67 |
|
|---|
| 68 |
|
|---|
| 69 |
|
|---|
| 70 |
|
|---|
| 71 |
|
|---|
| 72 |
|
|---|
| 73 |
|
|---|
| 74 |
|
|---|
| 75 |
|
|---|
| 76 |
|
|---|
| 77 |
|
|---|
| 78 |
|
|---|
| 79 |
|
|---|
| 80 |
|
|---|
| 81 |
(defgroup timeclock nil |
|---|
| 82 |
"Keeping track of the time that gets spent." |
|---|
| 83 |
:group 'data) |
|---|
| 84 |
|
|---|
| 85 |
|
|---|
| 86 |
|
|---|
| 87 |
(defcustom timeclock-file (convert-standard-filename "~/.timelog") |
|---|
| 88 |
"*The file used to store timeclock data in." |
|---|
| 89 |
:type 'file |
|---|
| 90 |
:group 'timeclock) |
|---|
| 91 |
|
|---|
| 92 |
(defcustom timeclock-workday (* 8 60 60) |
|---|
| 93 |
"*The length of a work period." |
|---|
| 94 |
:type 'integer |
|---|
| 95 |
:group 'timeclock) |
|---|
| 96 |
|
|---|
| 97 |
(defcustom timeclock-relative t |
|---|
| 98 |
"*Whether to make reported time relative to `timeclock-workday'. |
|---|
| 99 |
For example, if the length of a normal workday is eight hours, and you |
|---|
| 100 |
work four hours on Monday, then the amount of time \"remaining\" on |
|---|
| 101 |
Tuesday is twelve hours -- relative to an averaged work period of |
|---|
| 102 |
eight hours -- or eight hours, non-relative. So relative time takes |
|---|
| 103 |
into account any discrepancy of time under-worked or over-worked on |
|---|
| 104 |
previous days. This only affects the timeclock modeline display." |
|---|
| 105 |
:type 'boolean |
|---|
| 106 |
:group 'timeclock) |
|---|
| 107 |
|
|---|
| 108 |
(defcustom timeclock-get-project-function 'timeclock-ask-for-project |
|---|
| 109 |
"*The function used to determine the name of the current project. |
|---|
| 110 |
When clocking in, and no project is specified, this function will be |
|---|
| 111 |
called to determine what is the current project to be worked on. |
|---|
| 112 |
If this variable is nil, no questions will be asked." |
|---|
| 113 |
:type 'function |
|---|
| 114 |
:group 'timeclock) |
|---|
| 115 |
|
|---|
| 116 |
(defcustom timeclock-get-reason-function 'timeclock-ask-for-reason |
|---|
| 117 |
"*A function used to determine the reason for clocking out. |
|---|
| 118 |
When clocking out, and no reason is specified, this function will be |
|---|
| 119 |
called to determine what is the reason. |
|---|
| 120 |
If this variable is nil, no questions will be asked." |
|---|
| 121 |
:type 'function |
|---|
| 122 |
:group 'timeclock) |
|---|
| 123 |
|
|---|
| 124 |
(defcustom timeclock-get-workday-function nil |
|---|
| 125 |
"*A function used to determine the length of today's workday. |
|---|
| 126 |
The first time that a user clocks in each day, this function will be |
|---|
| 127 |
called to determine what is the length of the current workday. If |
|---|
| 128 |
the return value is nil, or equal to `timeclock-workday', nothing special |
|---|
| 129 |
will be done. If it is a quantity different from `timeclock-workday', |
|---|
| 130 |
however, a record will be output to the timelog file to note the fact that |
|---|
| 131 |
that day has a length that is different from the norm." |
|---|
| 132 |
:type '(choice (const nil) function) |
|---|
| 133 |
:group 'timeclock) |
|---|
| 134 |
|
|---|
| 135 |
(defcustom timeclock-ask-before-exiting t |
|---|
| 136 |
"*If non-nil, ask if the user wants to clock out before exiting Emacs. |
|---|
| 137 |
This variable only has effect if set with \\[customize]." |
|---|
| 138 |
:set (lambda (symbol value) |
|---|
| 139 |
(if value |
|---|
| 140 |
(add-hook 'kill-emacs-query-functions 'timeclock-query-out) |
|---|
| 141 |
(remove-hook 'kill-emacs-query-functions 'timeclock-query-out)) |
|---|
| 142 |
(setq timeclock-ask-before-exiting value)) |
|---|
| 143 |
:type 'boolean |
|---|
| 144 |
:group 'timeclock) |
|---|
| 145 |
|
|---|
| 146 |
(defvar timeclock-update-timer nil |
|---|
| 147 |
"The timer used to update `timeclock-mode-string'.") |
|---|
| 148 |
|
|---|
| 149 |
|
|---|
| 150 |
(defvar display-time-hook) |
|---|
| 151 |
(defvar timeclock-modeline-display) |
|---|
| 152 |
|
|---|
| 153 |
(defcustom timeclock-use-display-time t |
|---|
| 154 |
"*If non-nil, use `display-time-hook' for doing modeline updates. |
|---|
| 155 |
The advantage of this is that one less timer has to be set running |
|---|
| 156 |
amok in Emacs' process space. The disadvantage is that it requires |
|---|
| 157 |
you to have `display-time' running. If you don't want to use |
|---|
| 158 |
`display-time', but still want the modeline to show how much time is |
|---|
| 159 |
left, set this variable to nil. Changing the value of this variable |
|---|
| 160 |
while timeclock information is being displayed in the modeline has no |
|---|
| 161 |
effect. You should call the function `timeclock-modeline-display' with |
|---|
| 162 |
a positive argument to force an update." |
|---|
| 163 |
:set (lambda (symbol value) |
|---|
| 164 |
(let ((currently-displaying |
|---|
| 165 |
(and (boundp 'timeclock-modeline-display) |
|---|
| 166 |
timeclock-modeline-display))) |
|---|
| 167 |
|
|---|
| 168 |
|
|---|
| 169 |
|
|---|
| 170 |
|
|---|
| 171 |
(if (and currently-displaying |
|---|
| 172 |
(or (and value |
|---|
| 173 |
(boundp 'display-time-hook) |
|---|
| 174 |
(memq 'timeclock-update-modeline |
|---|
| 175 |
display-time-hook)) |
|---|
| 176 |
(and (not value) |
|---|
| 177 |
timeclock-update-timer))) |
|---|
| 178 |
(setq currently-displaying nil)) |
|---|
| 179 |
(and currently-displaying |
|---|
| 180 |
(set-variable 'timeclock-modeline-display nil)) |
|---|
| 181 |
(setq timeclock-use-display-time value) |
|---|
| 182 |
(and currently-displaying |
|---|
| 183 |
(set-variable 'timeclock-modeline-display t)) |
|---|
| 184 |
timeclock-use-display-time)) |
|---|
| 185 |
:type 'boolean |
|---|
| 186 |
:group 'timeclock |
|---|
| 187 |
:require 'time) |
|---|
| 188 |
|
|---|
| 189 |
(defcustom timeclock-first-in-hook nil |
|---|
| 190 |
"*A hook run for the first \"in\" event each day. |
|---|
| 191 |
Note that this hook is run before recording any events. Thus the |
|---|
| 192 |
value of `timeclock-hours-today', `timeclock-last-event' and the |
|---|
| 193 |
return value of function `timeclock-last-period' are relative previous |
|---|
| 194 |
to today." |
|---|
| 195 |
:type 'hook |
|---|
| 196 |
:group 'timeclock) |
|---|
| 197 |
|
|---|
| 198 |
(defcustom timeclock-load-hook nil |
|---|
| 199 |
"*Hook that gets run after timeclock has been loaded." |
|---|
| 200 |
:type 'hook |
|---|
| 201 |
:group 'timeclock) |
|---|
| 202 |
|
|---|
| 203 |
(defcustom timeclock-in-hook nil |
|---|
| 204 |
"*A hook run every time an \"in\" event is recorded." |
|---|
| 205 |
:type 'hook |
|---|
| 206 |
:group 'timeclock) |
|---|
| 207 |
|
|---|
| 208 |
(defcustom timeclock-day-over-hook nil |
|---|
| 209 |
"*A hook that is run when the workday has been completed. |
|---|
| 210 |
This hook is only run if the current time remaining is being displayed |
|---|
| 211 |
in the modeline. See the variable `timeclock-modeline-display'." |
|---|
| 212 |
:type 'hook |
|---|
| 213 |
:group 'timeclock) |
|---|
| 214 |
|
|---|
| 215 |
(defcustom timeclock-out-hook nil |
|---|
| 216 |
"*A hook run every time an \"out\" event is recorded." |
|---|
| 217 |
:type 'hook |
|---|
| 218 |
:group 'timeclock) |
|---|
| 219 |
|
|---|
| 220 |
(defcustom timeclock-done-hook nil |
|---|
| 221 |
"*A hook run every time a project is marked as completed." |
|---|
| 222 |
:type 'hook |
|---|
| 223 |
:group 'timeclock) |
|---|
| 224 |
|
|---|
| 225 |
(defcustom timeclock-event-hook nil |
|---|
| 226 |
"*A hook run every time any event is recorded." |
|---|
| 227 |
:type 'hook |
|---|
| 228 |
:group 'timeclock) |
|---|
| 229 |
|
|---|
| 230 |
(defvar timeclock-last-event nil |
|---|
| 231 |
"A list containing the last event that was recorded. |
|---|
| 232 |
The format of this list is (CODE TIME PROJECT).") |
|---|
| 233 |
|
|---|
| 234 |
(defvar timeclock-last-event-workday nil |
|---|
| 235 |
"The number of seconds in the workday of `timeclock-last-event'.") |
|---|
| 236 |
|
|---|
| 237 |
|
|---|
| 238 |
|
|---|
| 239 |
(defvar timeclock-discrepancy nil |
|---|
| 240 |
"A variable containing the time discrepancy before the last event. |
|---|
| 241 |
Normally, timeclock assumes that you intend to work for |
|---|
| 242 |
`timeclock-workday' seconds every day. Any days in which you work |
|---|
| 243 |
more or less than this amount is considered either a positive or |
|---|
| 244 |
a negative discrepancy. If you work in such a manner that the |
|---|
| 245 |
discrepancy is always brought back to zero, then you will by |
|---|
| 246 |
definition have worked an average amount equal to `timeclock-workday' |
|---|
| 247 |
each day.") |
|---|
| 248 |
|
|---|
| 249 |
(defvar timeclock-elapsed nil |
|---|
| 250 |
"A variable containing the time elapsed for complete periods today. |
|---|
| 251 |
This value is not accurate enough to be useful by itself. Rather, |
|---|
| 252 |
call `timeclock-workday-elapsed', to determine how much time has been |
|---|
| 253 |
worked so far today. Also, if `timeclock-relative' is nil, this value |
|---|
| 254 |
will be the same as `timeclock-discrepancy'.") |
|---|
| 255 |
|
|---|
| 256 |
(defvar timeclock-use-elapsed nil |
|---|
| 257 |
"Non-nil if the modeline should display time elapsed, not remaining.") |
|---|
| 258 |
|
|---|
| 259 |
(defvar timeclock-last-period nil |
|---|
| 260 |
"Integer representing the number of seconds in the last period. |
|---|
| 261 |
Note that you shouldn't access this value, but instead should use the |
|---|
| 262 |
function `timeclock-last-period'.") |
|---|
| 263 |
|
|---|
| 264 |
(defvar timeclock-mode-string nil |
|---|
| 265 |
"The timeclock string (optionally) displayed in the modeline. |
|---|
| 266 |
The time is bracketed by <> if you are clocked in, otherwise by [].") |
|---|
| 267 |
|
|---|
| 268 |
(defvar timeclock-day-over nil |
|---|
| 269 |
"The date of the last day when notified \"day over\" for.") |
|---|
| 270 |
|
|---|
| 271 |
|
|---|
| 272 |
|
|---|
| 273 |
|
|---|
| 274 |
(defun timeclock-modeline-display (&optional arg) |
|---|
| 275 |
"Toggle display of the amount of time left today in the modeline. |
|---|
| 276 |
If `timeclock-use-display-time' is non-nil (the default), then |
|---|
| 277 |
the function `display-time-mode' must be active, and the modeline |
|---|
| 278 |
will be updated whenever the time display is updated. Otherwise, |
|---|
| 279 |
the timeclock will use its own sixty second timer to do its |
|---|
| 280 |
updating. With prefix ARG, turn modeline display on if and only |
|---|
| 281 |
if ARG is positive. Returns the new status of timeclock modeline |
|---|
| 282 |
display (non-nil means on)." |
|---|
| 283 |
(interactive "P") |
|---|
| 284 |
|
|---|
| 285 |
(setq timeclock-mode-string "") |
|---|
| 286 |
(or global-mode-string (setq global-mode-string '(""))) |
|---|
| 287 |
(let ((on-p (if arg |
|---|
| 288 |
(> (prefix-numeric-value arg) 0) |
|---|
| 289 |
(not timeclock-modeline-display)))) |
|---|
| 290 |
(if on-p |
|---|
| 291 |
(progn |
|---|
| 292 |
(or (memq 'timeclock-mode-string global-mode-string) |
|---|
| 293 |
(setq global-mode-string |
|---|
| 294 |
(append global-mode-string '(timeclock-mode-string)))) |
|---|
| 295 |
(unless (memq 'timeclock-update-modeline timeclock-event-hook) |
|---|
| 296 |
(add-hook 'timeclock-event-hook 'timeclock-update-modeline)) |
|---|
| 297 |
(when timeclock-update-timer |
|---|
| 298 |
(cancel-timer timeclock-update-timer) |
|---|
| 299 |
(setq timeclock-update-timer nil)) |
|---|
| 300 |
(if (boundp 'display-time-hook) |
|---|
| 301 |
(remove-hook 'display-time-hook 'timeclock-update-modeline)) |
|---|
| 302 |
(if timeclock-use-display-time |
|---|
| 303 |
(progn |
|---|
| 304 |
|
|---|
| 305 |
|
|---|
| 306 |
(if display-time-mode (timeclock-update-modeline) |
|---|
| 307 |
(message "Activate `display-time-mode' or turn off \ |
|---|
| 308 |
`timeclock-use-display-time' to see timeclock information")) |
|---|
| 309 |
(add-hook 'display-time-hook 'timeclock-update-modeline)) |
|---|
| 310 |
(setq timeclock-update-timer |
|---|
| 311 |
(run-at-time nil 60 'timeclock-update-modeline)))) |
|---|
| 312 |
(setq global-mode-string |
|---|
| 313 |
(delq 'timeclock-mode-string global-mode-string)) |
|---|
| 314 |
(remove-hook 'timeclock-event-hook 'timeclock-update-modeline) |
|---|
| 315 |
(if (boundp 'display-time-hook) |
|---|
| 316 |
(remove-hook 'display-time-hook |
|---|
| 317 |
'timeclock-update-modeline)) |
|---|
| 318 |
(when timeclock-update-timer |
|---|
| 319 |
(cancel-timer timeclock-update-timer) |
|---|
| 320 |
(setq timeclock-update-timer nil))) |
|---|
| 321 |
(force-mode-line-update) |
|---|
| 322 |
(setq timeclock-modeline-display on-p))) |
|---|
| 323 |
|
|---|
| 324 |
|
|---|
| 325 |
|
|---|
| 326 |
(defcustom timeclock-modeline-display nil |
|---|
| 327 |
"Toggle modeline display of time remaining. |
|---|
| 328 |
You must modify via \\[customize] for this variable to have an effect." |
|---|
| 329 |
:set (lambda (symbol value) |
|---|
| 330 |
(setq timeclock-modeline-display |
|---|
| 331 |
(timeclock-modeline-display (or value 0)))) |
|---|
| 332 |
:type 'boolean |
|---|
| 333 |
:group 'timeclock |
|---|
| 334 |
:require 'timeclock) |
|---|
| 335 |
|
|---|
| 336 |
(defsubst timeclock-time-to-date (time) |
|---|
| 337 |
"Convert the TIME value to a textual date string." |
|---|
| 338 |
(format-time-string "%Y/%m/%d" time)) |
|---|
| 339 |
|
|---|
| 340 |
|
|---|
| 341 |
(defun timeclock-in (&optional arg project find-project) |
|---|
| 342 |
"Clock in, recording the current time moment in the timelog. |
|---|
| 343 |
With a numeric prefix ARG, record the fact that today has only that |
|---|
| 344 |
many hours in it to be worked. If arg is a non-numeric prefix arg |
|---|
| 345 |
\(non-nil, but not a number), 0 is assumed (working on a holiday or |
|---|
| 346 |
weekend). *If not called interactively, ARG should be the number of |
|---|
| 347 |
_seconds_ worked today*. This feature only has effect the first time |
|---|
| 348 |
this function is called within a day. |
|---|
| 349 |
|
|---|
| 350 |
PROJECT is the project being clocked into. If PROJECT is nil, and |
|---|
| 351 |
FIND-PROJECT is non-nil -- or the user calls `timeclock-in' |
|---|
| 352 |
interactively -- call the function `timeclock-get-project-function' to |
|---|
| 353 |
discover the name of the project." |
|---|
| 354 |
(interactive |
|---|
| 355 |
(list (and current-prefix-arg |
|---|
| 356 |
(if (numberp current-prefix-arg) |
|---|
| 357 |
(* current-prefix-arg 60 60) |
|---|
| 358 |
0)))) |
|---|
| 359 |
(if (equal (car timeclock-last-event) "i") |
|---|
| 360 |
(error "You've already clocked in!") |
|---|
| 361 |
(unless timeclock-last-event |
|---|
| 362 |
(timeclock-reread-log)) |
|---|
| 363 |
|
|---|
| 364 |
(unless (and timeclock-last-event |
|---|
| 365 |
(equal (timeclock-time-to-date |
|---|
| 366 |
(cadr timeclock-last-event)) |
|---|
| 367 |
(timeclock-time-to-date (current-time)))) |
|---|
| 368 |
(let ((workday (or (and (numberp arg) arg) |
|---|
| 369 |
(and arg 0) |
|---|
| 370 |
(and timeclock-get-workday-function |
|---|
| 371 |
(funcall timeclock-get-workday-function)) |
|---|
| 372 |
timeclock-workday))) |
|---|
| 373 |
(run-hooks 'timeclock-first-in-hook) |
|---|
| 374 |
|
|---|
| 375 |
(setq timeclock-discrepancy |
|---|
| 376 |
(- (or timeclock-discrepancy 0) workday)) |
|---|
| 377 |
(if (not (= workday timeclock-workday)) |
|---|
| 378 |
(timeclock-log "h" (number-to-string |
|---|
| 379 |
(/ workday (if (zerop (% workday (* 60 60))) |
|---|
| 380 |
60 60.0) 60)))))) |
|---|
| 381 |
(timeclock-log "i" (or project |
|---|
| 382 |
(and timeclock-get-project-function |
|---|
| 383 |
(or find-project (interactive-p)) |
|---|
| 384 |
(funcall timeclock-get-project-function)))) |
|---|
| 385 |
(run-hooks 'timeclock-in-hook))) |
|---|
| 386 |
|
|---|
| 387 |
|
|---|
| 388 |
(defun timeclock-out (&optional arg reason find-reason) |
|---|
| 389 |
"Clock out, recording the current time moment in the timelog. |
|---|
| 390 |
If a prefix ARG is given, the user has completed the project that was |
|---|
| 391 |
begun during the last time segment. |
|---|
| 392 |
|
|---|
| 393 |
REASON is the user's reason for clocking out. If REASON is nil, and |
|---|
| 394 |
FIND-REASON is non-nil -- or the user calls `timeclock-out' |
|---|
| 395 |
interactively -- call the function `timeclock-get-reason-function' to |
|---|
| 396 |
discover the reason." |
|---|
| 397 |
(interactive "P") |
|---|
| 398 |
(or timeclock-last-event |
|---|
| 399 |
(error "You haven't clocked in!")) |
|---|
| 400 |
(if (equal (downcase (car timeclock-last-event)) "o") |
|---|
| 401 |
(error "You've already clocked out!") |
|---|
| 402 |
(timeclock-log |
|---|
| 403 |
(if arg "O" "o") |
|---|
| 404 |
(or reason |
|---|
| 405 |
(and timeclock-get-reason-function |
|---|
| 406 |
(or find-reason (interactive-p)) |
|---|
| 407 |
(funcall timeclock-get-reason-function)))) |
|---|
| 408 |
(run-hooks 'timeclock-out-hook) |
|---|
| 409 |
(if arg |
|---|
| 410 |
(run-hooks 'timeclock-done-hook)))) |
|---|
| 411 |
|
|---|
| 412 |
|
|---|
| 413 |
(defsubst timeclock-workday-remaining (&optional today-only) |
|---|
| 414 |
"Return the number of seconds until the workday is complete. |
|---|
| 415 |
The amount returned is relative to the value of `timeclock-workday'. |
|---|
| 416 |
If TODAY-ONLY is non-nil, the value returned will be relative only to |
|---|
| 417 |
the time worked today, and not to past time." |
|---|
| 418 |
(let ((discrep (timeclock-find-discrep))) |
|---|
| 419 |
(if discrep |
|---|
| 420 |
(- (if today-only (cadr discrep) |
|---|
| 421 |
(car discrep))) |
|---|
| 422 |
0.0))) |
|---|
| 423 |
|
|---|
| 424 |
|
|---|
| 425 |
(defun timeclock-status-string (&optional show-seconds today-only) |
|---|
| 426 |
"Report the overall timeclock status at the present moment. |
|---|
| 427 |
If SHOW-SECONDS is non-nil, display second resolution. |
|---|
| 428 |
If TODAY-ONLY is non-nil, the display will be relative only to time |
|---|
| 429 |
worked today, ignoring the time worked on previous days." |
|---|
| 430 |
(interactive "P") |
|---|
| 431 |
(let ((remainder (timeclock-workday-remaining |
|---|
| 432 |
(or today-only |
|---|
| 433 |
(not timeclock-relative)))) |
|---|
| 434 |
(last-in (equal (car timeclock-last-event) "i")) |
|---|
| 435 |
status) |
|---|
| 436 |
(setq status |
|---|
| 437 |
(format "Currently %s since %s (%s), %s %s, leave at %s" |
|---|
| 438 |
(if last-in "IN" "OUT") |
|---|
| 439 |
(if show-seconds |
|---|
| 440 |
(format-time-string "%-I:%M:%S %p" |
|---|
| 441 |
(nth 1 timeclock-last-event)) |
|---|
| 442 |
(format-time-string "%-I:%M %p" |
|---|
| 443 |
(nth 1 timeclock-last-event))) |
|---|
| 444 |
(or (nth 2 timeclock-last-event) |
|---|
| 445 |
(if last-in "**UNKNOWN**" "workday over")) |
|---|
| 446 |
(timeclock-seconds-to-string remainder show-seconds t) |
|---|
| 447 |
(if (> remainder 0) |
|---|
| 448 |
"remaining" "over") |
|---|
| 449 |
(timeclock-when-to-leave-string show-seconds today-only))) |
|---|
| 450 |
(if (interactive-p) |
|---|
| 451 |
(message "%s" status) |
|---|
| 452 |
status))) |
|---|
| 453 |
|
|---|
| 454 |
|
|---|
| 455 |
(defun timeclock-change (&optional arg project) |
|---|
| 456 |
"Change to working on a different project. |
|---|
| 457 |
This clocks out of the current project, then clocks in on a new one. |
|---|
| 458 |
With a prefix ARG, consider the previous project as finished at the |
|---|
| 459 |
time of changeover. PROJECT is the name of the last project you were |
|---|
| 460 |
working on." |
|---|
| 461 |
(interactive "P") |
|---|
| 462 |
(timeclock-out arg) |
|---|
| 463 |
(timeclock-in nil project (interactive-p))) |
|---|
| 464 |
|
|---|
| 465 |
|
|---|
| 466 |
(defun timeclock-query-out () |
|---|
| 467 |
"Ask the user whether to clock out. |
|---|
| 468 |
This is a useful function for adding to `kill-emacs-query-functions'." |
|---|
| 469 |
(and (equal (car timeclock-last-event) "i") |
|---|
| 470 |
(y-or-n-p "You're currently clocking time, clock out? ") |
|---|
| 471 |
(timeclock-out)) |
|---|
| 472 |
|
|---|
| 473 |
t) |
|---|
| 474 |
|
|---|
| 475 |
|
|---|
| 476 |
(defun timeclock-reread-log () |
|---|
| 477 |
"Re-read the timeclock, to account for external changes. |
|---|
| 478 |
Returns the new value of `timeclock-discrepancy'." |
|---|
| 479 |
(interactive) |
|---|
| 480 |
(setq timeclock-discrepancy nil) |
|---|
| 481 |
(timeclock-find-discrep) |
|---|
| 482 |
(if (and timeclock-discrepancy timeclock-modeline-display) |
|---|
| 483 |
(timeclock-update-modeline)) |
|---|
| 484 |
timeclock-discrepancy) |
|---|
| 485 |
|
|---|
| 486 |
(defun timeclock-seconds-to-string (seconds &optional show-seconds |
|---|
| 487 |
reverse-leader) |
|---|
| 488 |
"Convert SECONDS into a compact time string. |
|---|
| 489 |
If SHOW-SECONDS is non-nil, make the resolution of the return string |
|---|
| 490 |
include the second count. If REVERSE-LEADER is non-nil, it means to |
|---|
| 491 |
output a \"+\" if the time value is negative, rather than a \"-\". |
|---|
| 492 |
This is used when negative time values have an inverted meaning (such |
|---|
| 493 |
as with time remaining, where negative time really means overtime)." |
|---|
| 494 |
(if show-seconds |
|---|
| 495 |
(format "%s%d:%02d:%02d" |
|---|
| 496 |
(if (< seconds 0) (if reverse-leader "+" "-") "") |
|---|
| 497 |
(truncate (/ (abs seconds) 60 60)) |
|---|
| 498 |
(% (truncate (/ (abs seconds) 60)) 60) |
|---|
| 499 |
(% (truncate (abs seconds)) 60)) |
|---|
| 500 |
(format "%s%d:%02d" |
|---|
| 501 |
(if (< seconds 0) (if reverse-leader "+" "-") "") |
|---|
| 502 |
(truncate (/ (abs seconds) 60 60)) |
|---|
| 503 |
(% (truncate (/ (abs seconds) 60)) 60)))) |
|---|
| 504 |
|
|---|
| 505 |
(defsubst timeclock-currently-in-p () |
|---|
| 506 |
"Return non-nil if the user is currently clocked in." |
|---|
| 507 |
(equal (car timeclock-last-event) "i")) |
|---|
| 508 |
|
|---|
| 509 |
|
|---|
| 510 |
(defun timeclock-workday-remaining-string (&optional show-seconds |
|---|
| 511 |
today-only) |
|---|
| 512 |
"Return a string representing the amount of time left today. |
|---|
| 513 |
Display second resolution if SHOW-SECONDS is non-nil. If TODAY-ONLY |
|---|
| 514 |
is non-nil, the display will be relative only to time worked today. |
|---|
| 515 |
See `timeclock-relative' for more information about the meaning of |
|---|
| 516 |
\"relative to today\"." |
|---|
| 517 |
(interactive) |
|---|
| 518 |
(let ((string (timeclock-seconds-to-string |
|---|
| 519 |
(timeclock-workday-remaining today-only) |
|---|
| 520 |
show-seconds t))) |
|---|
| 521 |
(if (interactive-p) |
|---|
| 522 |
(message "%s" string) |
|---|
| 523 |
string))) |
|---|
| 524 |
|
|---|
| 525 |
(defsubst timeclock-workday-elapsed () |
|---|
| 526 |
"Return the number of seconds worked so far today. |
|---|
| 527 |
If RELATIVE is non-nil, the amount returned will be relative to past |
|---|
| 528 |
time worked. The default is to return only the time that has elapsed |
|---|
| 529 |
so far today." |
|---|
| 530 |
(let ((discrep (timeclock-find-discrep))) |
|---|
| 531 |
(if discrep |
|---|
| 532 |
(nth 2 discrep) |
|---|
| 533 |
0.0))) |
|---|
| 534 |
|
|---|
| 535 |
|
|---|
| 536 |
(defun timeclock-workday-elapsed-string (&optional show-seconds) |
|---|
| 537 |
"Return a string representing the amount of time worked today. |
|---|
| 538 |
Display seconds resolution if SHOW-SECONDS is non-nil. If RELATIVE is |
|---|
| 539 |
non-nil, the amount returned will be relative to past time worked." |
|---|
| 540 |
(interactive) |
|---|
| 541 |
(let ((string (timeclock-seconds-to-string (timeclock-workday-elapsed) |
|---|
| 542 |
show-seconds))) |
|---|
| 543 |
(if (interactive-p) |
|---|
| 544 |
(message "%s" string) |
|---|
| 545 |
string))) |
|---|
| 546 |
|
|---|
| 547 |
(defsubst timeclock-time-to-seconds (time) |
|---|
| 548 |
"Convert TIME to a floating point number." |
|---|
| 549 |
(+ (* (car time) 65536.0) |
|---|
| 550 |
(cadr time) |
|---|
| 551 |
(/ (or (car (cdr (cdr time))) 0) 1000000.0))) |
|---|
| 552 |
|
|---|
| 553 |
(defsubst timeclock-seconds-to-time (seconds) |
|---|
| 554 |
"Convert SECONDS (a floating point number) to an Emacs time structure." |
|---|
| 555 |
(list (floor seconds 65536) |
|---|
| 556 |
(floor (mod seconds 65536)) |
|---|
| 557 |
(floor (* (- seconds (ffloor seconds)) 1000000)))) |
|---|
| 558 |
|
|---|
| 559 |
|
|---|
| 560 |
(defsubst timeclock-when-to-leave (&optional today-only) |
|---|
| 561 |
"Return a time value representing the end of today's workday. |
|---|
| 562 |
If TODAY-ONLY is non-nil, the value returned will be relative only to |
|---|
| 563 |
the time worked today, and not to past time." |
|---|
| 564 |
(timeclock-seconds-to-time |
|---|
| 565 |
(- (timeclock-time-to-seconds (current-time)) |
|---|
| 566 |
(let ((discrep (timeclock-find-discrep))) |
|---|
| 567 |
(if discrep |
|---|
| 568 |
(if today-only |
|---|
| 569 |
(cadr discrep) |
|---|
| 570 |
(car discrep)) |
|---|
| 571 |
0.0))))) |
|---|
| 572 |
|
|---|
| 573 |
|
|---|
| 574 |
(defun timeclock-when-to-leave-string (&optional show-seconds |
|---|
| 575 |
today-only) |
|---|
| 576 |
"Return a string representing the end of today's workday. |
|---|
| 577 |
This string is relative to the value of `timeclock-workday'. If |
|---|
| 578 |
SHOW-SECONDS is non-nil, the value printed/returned will include |
|---|
| 579 |
seconds. If TODAY-ONLY is non-nil, the value returned will be |
|---|
| 580 |
relative only to the time worked today, and not to past time." |
|---|
| 581 |
|
|---|
| 582 |
(interactive) |
|---|
| 583 |
(let* ((then (timeclock-when-to-leave today-only)) |
|---|
| 584 |
(string |
|---|
| 585 |
(if show-seconds |
|---|
| 586 |
(format-time-string "%-I:%M:%S %p" then) |
|---|
| 587 |
(format-time-string "%-I:%M %p" then)))) |
|---|
| 588 |
(if (interactive-p) |
|---|
| 589 |
(message "%s" string) |
|---|
| 590 |
string))) |
|---|
| 591 |
|
|---|
| 592 |
(defun timeclock-make-hours-explicit (old-default) |
|---|
| 593 |
"Specify all workday lengths in `timeclock-file'. |
|---|
| 594 |
OLD-DEFAULT hours are set for every day that has no number indicated." |
|---|
| 595 |
(interactive "P") |
|---|
| 596 |
(if old-default (setq old-default (prefix-numeric-value old-default)) |
|---|
| 597 |
(error "timelog-make-hours-explicit requires an explicit argument")) |
|---|
| 598 |
(let ((extant-timelog (find-buffer-visiting timeclock-file)) |
|---|
| 599 |
current-date) |
|---|
| 600 |
(with-current-buffer (find-file-noselect timeclock-file t) |
|---|
| 601 |
(unwind-protect |
|---|
| 602 |
(save-excursion |
|---|
| 603 |
(save-restriction |
|---|
| 604 |
(widen) |
|---|
| 605 |
(goto-char (point-min)) |
|---|
| 606 |
(while (progn (skip-chars-forward "\n") (not (eobp))) |
|---|
| 607 |
|
|---|
| 608 |
(unless (looking-at |
|---|
| 609 |
(concat "^\\([bhioO]\\) \\([0-9]+/[0-9]+/[0-9]+\\) " |
|---|
| 610 |
"\\([0-9]+:[0-9]+:[0-9]+\\)")) |
|---|
| 611 |
(error "Can't parse `%s'" timeclock-file)) |
|---|
| 612 |
(let ((this-date (match-string 2))) |
|---|
| 613 |
(unless (or (and current-date |
|---|
| 614 |
(string= this-date current-date)) |
|---|
| 615 |
(string= (match-string 1) "h")) |
|---|
| 616 |
(insert (format "h %s %s %s\n" (match-string 2) |
|---|
| 617 |
(match-string 3) old-default))) |
|---|
| 618 |
(if (string-match "^[ih]" (match-string 1)) |
|---|
| 619 |
(setq current-date this-date))) |
|---|
| 620 |
(forward-line)) |
|---|
| 621 |
(save-buffer))) |
|---|
| 622 |
(unless extant-timelog (kill-buffer (current-buffer))))))) |
|---|
| 623 |
|
|---|
| 624 |
|
|---|
| 625 |
|
|---|
| 626 |
(defvar timeclock-project-list nil) |
|---|
| 627 |
(defvar timeclock-last-project nil) |
|---|
| 628 |
|
|---|
| 629 |
(defun timeclock-completing-read (prompt alist &optional default) |
|---|
| 630 |
"A version of `completing-read' that works on both Emacs and XEmacs." |
|---|
| 631 |
(if (featurep 'xemacs) |
|---|
| 632 |
(let ((str (completing-read prompt alist))) |
|---|
| 633 |
(if (or (null str) (= (length str) 0)) |
|---|
| 634 |
default |
|---|
| 635 |
str)) |
|---|
| 636 |
(completing-read prompt alist nil nil nil nil default))) |
|---|
| 637 |
|
|---|
| 638 |
(defun timeclock-ask-for-project () |
|---|
| 639 |
"Ask the user for the project they are clocking into." |
|---|
| 640 |
(timeclock-completing-read |
|---|
| 641 |
(format "Clock into which project (default %s): " |
|---|
| 642 |
(or timeclock-last-project |
|---|
| 643 |
(car timeclock-project-list))) |
|---|
| 644 |
(mapcar 'list timeclock-project-list) |
|---|
| 645 |
(or timeclock-last-project |
|---|
| 646 |
(car timeclock-project-list)))) |
|---|
| 647 |
|
|---|
| 648 |
(defvar timeclock-reason-list nil) |
|---|
| 649 |
|
|---|
| 650 |
(defun timeclock-ask-for-reason () |
|---|
| 651 |
"Ask the user for the reason they are clocking out." |
|---|
| 652 |
(timeclock-completing-read "Reason for clocking out: " |
|---|
| 653 |
(mapcar 'list timeclock-reason-list))) |
|---|
| 654 |
|
|---|
| 655 |
(defun timeclock-update-modeline () |
|---|
| 656 |
"Update the `timeclock-mode-string' displayed in the modeline. |
|---|
| 657 |
The value of `timeclock-relative' affects the display as described in |
|---|
| 658 |
that variable's documentation." |
|---|
| 659 |
(interactive) |
|---|
| 660 |
(let ((remainder |
|---|
| 661 |
(if timeclock-use-elapsed |
|---|
| 662 |
(timeclock-workday-elapsed) |
|---|
| 663 |
(timeclock-workday-remaining (not timeclock-relative)))) |
|---|
| 664 |
(last-in (equal (car timeclock-last-event) "i"))) |
|---|
| 665 |
(when (and (< remainder 0) |
|---|
| 666 |
(not (and timeclock-day-over |
|---|
| 667 |
(equal timeclock-day-over |
|---|
| 668 |
(timeclock-time-to-date |
|---|
| 669 |
(current-time)))))) |
|---|
| 670 |
(setq timeclock-day-over |
|---|
| 671 |
(timeclock-time-to-date (current-time))) |
|---|
| 672 |
(run-hooks 'timeclock-day-over-hook)) |
|---|
| 673 |
(setq timeclock-mode-string |
|---|
| 674 |
(propertize |
|---|
| 675 |
(format " %c%s%c " |
|---|
| 676 |
(if last-in ?< ?[) |
|---|
| 677 |
(timeclock-seconds-to-string remainder nil t) |
|---|
| 678 |
(if last-in ?> ?])) |
|---|
| 679 |
'help-echo "timeclock: time remaining")))) |
|---|
| 680 |
|
|---|
| 681 |
(put 'timeclock-mode-string 'risky-local-variable t) |
|---|
| 682 |
|
|---|
| 683 |
(defun timeclock-log (code &optional project) |
|---|
| 684 |
"Log the event CODE to the timeclock log, at the time of call. |
|---|
| 685 |
If PROJECT is a string, it represents the project which the event is |
|---|
| 686 |
being logged for. Normally only \"in\" events specify a project." |
|---|
| 687 |
(let ((extant-timelog (find-buffer-visiting timeclock-file))) |
|---|
| 688 |
(with-current-buffer (find-file-noselect timeclock-file t) |
|---|
| 689 |
(save-excursion |
|---|
| 690 |
(save-restriction |
|---|
| 691 |
(widen) |
|---|
| 692 |
(goto-char (point-max)) |
|---|
| 693 |
(if (not (bolp)) |
|---|
| 694 |
(insert "\n")) |
|---|
| 695 |
(let ((now (current-time))) |
|---|
| 696 |
(insert code " " |
|---|
| 697 |
(format-time-string "%Y/%m/%d %H:%M:%S" now) |
|---|
| 698 |
(or (and (stringp project) |
|---|
| 699 |
(> (length project) 0) |
|---|
| 700 |
(concat " " project)) |
|---|
| 701 |
"") |
|---|
| 702 |
"\n") |
|---|
| 703 |
(if (equal (downcase code) "o") |
|---|
| 704 |
(setq timeclock-last-period |
|---|
| 705 |
(- (timeclock-time-to-seconds now) |
|---|
| 706 |
(timeclock-time-to-seconds |
|---|
| 707 |
(cadr timeclock-last-event))) |
|---|
| 708 |
timeclock-discrepancy |
|---|
| 709 |
(+ timeclock-discrepancy |
|---|
| 710 |
timeclock-last-period))) |
|---|
| 711 |
(setq timeclock-last-event (list code now project))))) |
|---|
| 712 |
(save-buffer) |
|---|
| 713 |
(unless extant-timelog (kill-buffer (current-buffer))))) |
|---|
| 714 |
(run-hooks 'timeclock-event-hook)) |
|---|
| 715 |
|
|---|
| 716 |
(defvar timeclock-moment-regexp |
|---|
| 717 |
(concat "\\([bhioO]\\)\\s-+" |
|---|
| 718 |
"\\([0-9]+\\)/\\([0-9]+\\)/\\([0-9]+\\)\\s-+" |
|---|
| 719 |
"\\([0-9]+\\):\\([0-9]+\\):\\([0-9]+\\)[ \t]*" "\\([^\n]*\\)")) |
|---|
| 720 |
|
|---|
| 721 |
(defsubst timeclock-read-moment () |
|---|
| 722 |
"Read the moment under point from the timelog." |
|---|
| 723 |
(if (looking-at timeclock-moment-regexp) |
|---|
| 724 |
(let ((code (match-string 1)) |
|---|
| 725 |
(year (string-to-number (match-string 2))) |
|---|
| 726 |
(mon (string-to-number (match-string 3))) |
|---|
| 727 |
(mday (string-to-number (match-string 4))) |
|---|
| 728 |
(hour (string-to-number (match-string 5))) |
|---|
| 729 |
(min (string-to-number (match-string 6))) |
|---|
| 730 |
(sec (string-to-number (match-string 7))) |
|---|
| 731 |
(project (match-string 8))) |
|---|
| 732 |
(list code (encode-time sec min hour mday mon year) project)))) |
|---|
| 733 |
|
|---|
| 734 |
(defun timeclock-last-period (&optional moment) |
|---|
| 735 |
"Return the value of the last event period. |
|---|
| 736 |
If the last event was a clock-in, the period will be open ended, and |
|---|
| 737 |
growing every second. Otherwise, it is a fixed amount which has been |
|---|
| 738 |
recorded to disk. If MOMENT is non-nil, use that as the current time. |
|---|
| 739 |
This is only provided for coherency when used by |
|---|
| 740 |
`timeclock-discrepancy'." |
|---|
| 741 |
(if (equal (car timeclock-last-event) "i") |
|---|
| 742 |
(- (timeclock-time-to-seconds (or moment (current-time))) |
|---|
| 743 |
(timeclock-time-to-seconds |
|---|
| 744 |
(cadr timeclock-last-event))) |
|---|
| 745 |
timeclock-last-period)) |
|---|
| 746 |
|
|---|
| 747 |
(defsubst timeclock-entry-length (entry) |
|---|
| 748 |
(- (timeclock-time-to-seconds (cadr entry)) |
|---|
| 749 |
(timeclock-time-to-seconds (car entry)))) |
|---|
| 750 |
|
|---|
| 751 |
(defsubst timeclock-entry-begin (entry) |
|---|
| 752 |
(car entry)) |
|---|
| 753 |
|
|---|
| 754 |
(defsubst timeclock-entry-end (entry) |
|---|
| 755 |
(cadr entry)) |
|---|
| 756 |
|
|---|
| 757 |
(defsubst timeclock-entry-project (entry) |
|---|
| 758 |
(nth 2 entry)) |
|---|
| 759 |
|
|---|
| 760 |
(defsubst timeclock-entry-comment (entry) |
|---|
| 761 |
(nth 3 entry)) |
|---|
| 762 |
|
|---|
| 763 |
|
|---|
| 764 |
(defsubst timeclock-entry-list-length (entry-list) |
|---|
| 765 |
(let ((length 0)) |
|---|
| 766 |
(while entry-list |
|---|
| 767 |
(setq length (+ length (timeclock-entry-length (car entry-list)))) |
|---|
| 768 |
(setq entry-list (cdr entry-list))) |
|---|
| 769 |
length)) |
|---|
| 770 |
|
|---|
| 771 |
(defsubst timeclock-entry-list-begin (entry-list) |
|---|
| 772 |
(timeclock-entry-begin (car entry-list))) |
|---|
| 773 |
|
|---|
| 774 |
(defsubst timeclock-entry-list-end (entry-list) |
|---|
| 775 |
(timeclock-entry-end (car (last entry-list)))) |
|---|
| 776 |
|
|---|
| 777 |
(defsubst timeclock-entry-list-span (entry-list) |
|---|
| 778 |
(- (timeclock-time-to-seconds (timeclock-entry-list-end entry-list)) |
|---|
| 779 |
(timeclock-time-to-seconds (timeclock-entry-list-begin entry-list)))) |
|---|
| 780 |
|
|---|
| 781 |
(defsubst timeclock-entry-list-break (entry-list) |
|---|
| 782 |
(- (timeclock-entry-list-span entry-list) |
|---|
| 783 |
(timeclock-entry-list-length entry-list))) |
|---|
| 784 |
|
|---|
| 785 |
(defsubst timeclock-entry-list-projects (entry-list) |
|---|
| 786 |
(let (projects) |
|---|
| 787 |
(while entry-list |
|---|
| 788 |
(let ((project (timeclock-entry-project (car entry-list)))) |
|---|
| 789 |
(if projects |
|---|
| 790 |
(add-to-list 'projects project) |
|---|
| 791 |
(setq projects (list project)))) |
|---|
| 792 |
(setq entry-list (cdr entry-list))) |
|---|
| 793 |
projects)) |
|---|
| 794 |
|
|---|
| 795 |
|
|---|
| 796 |
(defsubst timeclock-day-required (day) |
|---|
| 797 |
(or (car day) timeclock-workday)) |
|---|
| 798 |
|
|---|
| 799 |
(defsubst timeclock-day-length (day) |
|---|
| 800 |
(timeclock-entry-list-length (cdr day))) |
|---|
| 801 |
|
|---|
| 802 |
(defsubst timeclock-day-debt (day) |
|---|
| 803 |
(- (timeclock-day-required day) |
|---|
| 804 |
(timeclock-day-length day))) |
|---|
| 805 |
|
|---|
| 806 |
(defsubst timeclock-day-begin (day) |
|---|
| 807 |
(timeclock-entry-list-begin (cdr day))) |
|---|
| 808 |
|
|---|
| 809 |
(defsubst timeclock-day-end (day) |
|---|
| 810 |
(timeclock-entry-list-end (cdr day))) |
|---|
| 811 |
|
|---|
| 812 |
(defsubst timeclock-day-span (day) |
|---|
| 813 |
(timeclock-entry-list-span (cdr day))) |
|---|
| 814 |
|
|---|
| 815 |
(defsubst timeclock-day-break (day) |
|---|
| 816 |
(timeclock-entry-list-break (cdr day))) |
|---|
| 817 |
|
|---|
| 818 |
(defsubst timeclock-day-projects (day) |
|---|
| 819 |
(timeclock-entry-list-projects (cdr day))) |
|---|
| 820 |
|
|---|
| 821 |
(defmacro timeclock-day-list-template (func) |
|---|
| 822 |
`(let ((length 0)) |
|---|
| 823 |
(while day-list |
|---|
| 824 |
(setq length (+ length (,(eval func) (car day-list)))) |
|---|
| 825 |
(setq day-list (cdr day-list))) |
|---|
| 826 |
length)) |
|---|
| 827 |
|
|---|
| 828 |
(defun timeclock-day-list-required (day-list) |
|---|
| 829 |
(timeclock-day-list-template 'timeclock-day-required)) |
|---|
| 830 |
|
|---|
| 831 |
(defun timeclock-day-list-length (day-list) |
|---|
| 832 |
(timeclock-day-list-template 'timeclock-day-length)) |
|---|
| 833 |
|
|---|
| 834 |
(defun timeclock-day-list-debt (day-list) |
|---|
| 835 |
(timeclock-day-list-template 'timeclock-day-debt)) |
|---|
| 836 |
|
|---|
| 837 |
(defsubst timeclock-day-list-begin (day-list) |
|---|
| 838 |
(timeclock-day-begin (car day-list))) |
|---|
| 839 |
|
|---|
| 840 |
(defsubst timeclock-day-list-end (day-list) |
|---|
| 841 |
(timeclock-day-end (car (last day-list)))) |
|---|
| 842 |
|
|---|
| 843 |
(defun timeclock-day-list-span (day-list) |
|---|
| 844 |
(timeclock-day-list-template 'timeclock-day-span)) |
|---|
| 845 |
|
|---|
| 846 |
(defun timeclock-day-list-break (day-list) |
|---|
| 847 |
(timeclock-day-list-template 'timeclock-day-break)) |
|---|
|
|---|