|
1 ; timeclock functionnality |
|
2 (require 'timeclock) |
|
3 (require 'calendar) |
|
4 |
|
5 ;(timeclock-modeline-display) |
|
6 (add-hook 'kill-emacs-hook 'timeclock-query-out) |
|
7 |
|
8 ;; planner mode |
|
9 |
|
10 ;;; helper functions |
|
11 |
|
12 (defun planner-date-to-calendar (date) |
|
13 "Convert a planner date string like \"YYYY.MM.DD\" in |
|
14 a calendar date list (month day year)." |
|
15 (let ((split-date |
|
16 (mapcar 'string-to-number |
|
17 (split-string (file-name-nondirectory date) "\\.")))) |
|
18 (list (nth 1 split-date) |
|
19 (nth 2 split-date) |
|
20 (nth 0 split-date)))) |
|
21 |
|
22 (defsubst calendar-date-to-planner (date) |
|
23 ; "Convert a planner date string like \"YYYY.MM.DD\" in |
|
24 ; a calendar date list (month day year)." |
|
25 (format "%04d.%02d.%02d" |
|
26 (nth 2 date) |
|
27 (nth 0 date) |
|
28 (nth 1 date))) |
|
29 |
|
30 ;; Moving functions |
|
31 |
|
32 (defun planner-goto-schedule () |
|
33 "Create if necessary and go to a Schedule section in current planner. |
|
34 Ensure to preserved the buffer-modified-p value so that a kill will not |
|
35 preserve the automated change for nothing." |
|
36 (interactive) |
|
37 (goto-char (point-min)) |
|
38 (let ((modified (buffer-modified-p))) |
|
39 (unless (re-search-forward "^\\* Schedule\n\n" nil t) |
|
40 (re-search-forward "^\\* Notes") |
|
41 (beginning-of-line) |
|
42 (insert "* Schedule\n\n\n\n") |
|
43 (forward-line -2) |
|
44 (set-buffer-modified-p modified)))) |
|
45 |
|
46 (defun planner-goto-events () |
|
47 "Create if necessary and go to a Events section just before the |
|
48 Schedule one (which is also create if necessary) in current planner. |
|
49 Ensure to preserved the buffer-modified-p value so that a kill will |
|
50 not preserve the automated change for nothing." |
|
51 (interactive) |
|
52 (goto-char (point-min)) |
|
53 (let ((modified (buffer-modified-p))) |
|
54 (unless (re-search-forward "^\\* Events\n\n" nil t) |
|
55 (planner-goto-schedule) |
|
56 (re-search-backward "^\\* Schedule") |
|
57 (insert "* Events\n\n\n\n") |
|
58 (forward-line -2) |
|
59 (set-buffer-modified-p modified)))) |
|
60 |
|
61 ;; The reminders converter. |
|
62 ;; |
|
63 ;; It now used a custom diary file, so you |
|
64 ;; can included it without breaking your normal diary-file. |
|
65 ;; The function can also set some arguments. I used it to call |
|
66 ;; a modified remconv which accept a "euro" argument to output |
|
67 ;; euro style date. |
|
68 ;; |
|
69 ;; I no more used those ones now since I'm directly marking the |
|
70 ;; calendar and the add-appt from the planner files. |
|
71 |
|
72 (defcustom planner-diary-file diary-file |
|
73 "Diary file where to add planner entries. You can set this value to |
|
74 another file than `diary-file' and add in `diary-file' the #include |
|
75 \"~/your-planner-diary-file-name\". Just make sure that you call |
|
76 `planner-convert-reminders' before `include-other-diary-files' in the |
|
77 `list-diary-entries-hook'." |
|
78 :type 'file |
|
79 :group 'planner) |
|
80 |
|
81 (defcustom planner-remconv-command "remconv" |
|
82 "A command that output diary compatible entries. Can take arguments |
|
83 also." |
|
84 :type 'string |
|
85 :group 'planner) |
|
86 |
|
87 (defun planner-convert-reminders () |
|
88 "Generate a diary file from a .reminders file. |
|
89 You can add this this function to the `list-diary-entries-hook'" |
|
90 (with-current-buffer (find-file-noselect planner-diary-file) |
|
91 (erase-buffer) |
|
92 (insert (shell-command-to-string planner-remconv-command)) |
|
93 (save-buffer))) |
|
94 |
|
95 ;; redefinition of planner-maybe-remove-file |
|
96 ;; |
|
97 ;; The kill-buffer of this function make me crazy. I modify it so you |
|
98 ;; can change it's behavior using this custom variable. Personnaly, I |
|
99 ;; used ignore and let emacs ask me to save it when I kill it. Take |
|
100 ;; also note that I change the regexp to remove any file that only |
|
101 ;; have empty section (section with just a first level heading). This |
|
102 ;; is to cope with the new Events and Schedule sections. |
|
103 |
|
104 (defcustom planner-not-empty-file 'kill-this-buffer |
|
105 "Command to run when a planner file is not empty. Can be |
|
106 kill-this-buffer, save-buffer or ignore for example." |
|
107 :type 'function |
|
108 :group 'planner) |
|
109 |
|
110 (defvar local-planner-empty-line-regexp "\\(\\* .*\\|[[:space:]]*\\)") |
|
111 (defvar planner-empty-file-regexp |
|
112 (concat "\\(^" local-planner-empty-line-regexp |
|
113 "\n\\)*[[:space:]]*\\'")) |
|
114 |
|
115 ; redefinition |
|
116 |
|
117 (defun planner-maybe-remove-file () |
|
118 "This function remove the file if it contains only first level |
|
119 headings and empty lines." |
|
120 (interactive) |
|
121 (goto-char (point-min)) |
|
122 (if (looking-at planner-empty-file-regexp) |
|
123 (let ((filename buffer-file-name)) |
|
124 (set-buffer-modified-p nil) |
|
125 (kill-buffer (current-buffer)) |
|
126 (delete-file filename)) |
|
127 (funcall planner-not-empty-file))) |
|
128 |
|
129 ;; planner-browse-url bbdb url handling |
|
130 ;; |
|
131 ;; My emacs-wiki doesn't like bbdb url like [[bbdb://Fabien Niņoles]] |
|
132 ;; I make this advice so he can replace _ with space and __ with _ in the |
|
133 ;; url before letting planner-browse-url handling it. |
|
134 |
|
135 (defadvice planner-browse-url (before local-bbdb-url-pretreatment (url)) |
|
136 "Pretreatment of some URL." |
|
137 (if (string-match "^bbdb://\\(.+\\)" url) |
|
138 (setq url |
|
139 (mapconcat |
|
140 '(lambda (str) (subst-char-in-string ?_ ? str t)) |
|
141 (split-string url "__") |
|
142 "_")))) |
|
143 |
|
144 (ad-activate 'planner-browse-url) |
|
145 |
|
146 ;; diary support functions |
|
147 ;; |
|
148 ;; This functions add diary entries into the Schedule and Events |
|
149 ;; sections of a planner buffer. I hook it to the |
|
150 ;; `planner-seek-to-first' function and only if the Events section |
|
151 ;; doesn't already exist. The best should be that it check if the |
|
152 ;; entry doesn't exist already instead but currently it works pretty |
|
153 ;; good. You can always call it interactively if you want to update |
|
154 ;; your Events/Schedule. Just make sure that you don't add the |
|
155 ;; `planner-convert-reminder' set in the `list-diary-entries-hook' |
|
156 ;; setup. |
|
157 |
|
158 (defun planner-insert-diary () |
|
159 "Insert the diary schedule in the planner buffer." |
|
160 (interactive) |
|
161 (let* |
|
162 ((entries (list-diary-entries (planner-date-to-calendar (emacs-wiki-page-name)) 1)) |
|
163 (events)) |
|
164 (planner-goto-schedule) |
|
165 (while entries |
|
166 (let* ((entry (nth 1 (car entries))) |
|
167 (lines (split-string entry "\n")) |
|
168 (line)) |
|
169 (while (setq line (car lines)) |
|
170 (if (string-match |
|
171 "^[[:space:]]*\\([0-9]+:[0-9]\\{2\\}\\(?:am\\|pm\\)?\\)\\(-[0-9]+:[0-9]\\{2\\}\\(?:am\\|pm\\)?\\)?[[:space:]]+\\(.*\\)$" |
|
172 line) |
|
173 (let ((starttime (match-string 1 line)) |
|
174 (endtime (match-string 2 line)) |
|
175 (description (match-string 3 line))) |
|
176 (insert (concat " " starttime |
|
177 " | " description |
|
178 (if endtime |
|
179 (let |
|
180 ((minutes |
|
181 (- (appt-convert-time (substring endtime 1 nil)) |
|
182 (appt-convert-time starttime)))) |
|
183 (format " (%d:%02d)" |
|
184 (/ minutes 60) |
|
185 (% minutes 60))) |
|
186 "") |
|
187 "\n"))) |
|
188 (setq events (cons line events))) |
|
189 (setq lines (cdr lines)))) |
|
190 (setq entries (cdr entries))) |
|
191 (planner-goto-events) |
|
192 (while events |
|
193 (insert (concat " - " (car events) "\n")) |
|
194 (setq events (cdr events))))) |
|
195 |
|
196 (defadvice planner-seek-to-first (after planner-insert-diary-ad ()) |
|
197 "Insert the diary into a newly create buffer." |
|
198 (if (string-match planner-date-regexp (emacs-wiki-page-name)) |
|
199 (save-excursion |
|
200 (goto-char (point-min)) |
|
201 (if (not (re-search-forward "^\\* Events[[:space:]]*$" nil t)) |
|
202 (planner-insert-diary))))) |
|
203 |
|
204 (ad-activate 'planner-seek-to-first) |
|
205 |
|
206 ;; appt support |
|
207 ;; |
|
208 ;; I'm loading now my appointments directly from the planner list. It |
|
209 ;; has the same functionnality as the regular appt-make-list, |
|
210 ;; including its own planner-prev-appt-check variable. It's set as an |
|
211 ;; after-advice to appt-check. I also add a function that parse the |
|
212 ;; current line and add the appt to it's list. The |
|
213 ;; planner-appt-entry-regexp are setup to catch any 'HH:MM | Message |
|
214 ;; (HH:MM)' line (just like allrems). You can set it up also to ignore |
|
215 ;; some special line (like those beginning with '&' for example). |
|
216 |
|
217 (require 'appt) |
|
218 |
|
219 (defvar planner-appt-entry-regexp |
|
220 "^[[:space:]]*\\([0-9]+:[0-9]\\{2\\}\\)[[:space:]]*|[[:space:]]*\\([^&].*\\)[[:space:]]*\\(([0-9]+:[0-9]\\{2\\})\\)?[[:space:]]*$" |
|
221 "Regexp that match a appt entry in planner.") |
|
222 |
|
223 (defun planner-appt-add () |
|
224 "Add this line as an appointment. The line should match |
|
225 `planner-appt-entry-regexp'." |
|
226 (interactive) |
|
227 (save-excursion |
|
228 (beginning-of-line) |
|
229 (if (looking-at planner-appt-entry-regexp) |
|
230 (appt-add (match-string-no-properties 1) (match-string-no-properties 2)) |
|
231 (error "No appointment on this line")))) |
|
232 |
|
233 (defun planner-make-appt-list () |
|
234 "Load today planner file and make appt-list from schedule." |
|
235 (interactive) |
|
236 (save-excursion |
|
237 (save-window-excursion |
|
238 (planner-goto-today) |
|
239 (while (re-search-forward planner-appt-entry-regexp nil t) |
|
240 (appt-add (match-string-no-properties 1) (match-string-no-properties 2)))))) |
|
241 |
|
242 (defvar planner-prev-appt-check nil |
|
243 "Determine the last time appt-check are called") |
|
244 |
|
245 (defadvice appt-check (after planner-appt-check-ad ()) |
|
246 "Call planner-make-appt-list the first time and every day." |
|
247 (let* ((now (decode-time)) |
|
248 (cur-hour (nth 2 now)) |
|
249 (cur-min (nth 1 now)) |
|
250 (cur-comp-time (+ (* cur-hour 60) cur-min))) |
|
251 (if (or (null planner-prev-appt-check) |
|
252 (< cur-comp-time planner-prev-appt-check)) |
|
253 (planner-make-appt-list)) |
|
254 (setq planner-prev-appt-check cur-comp-time))) |
|
255 |
|
256 (ad-activate 'appt-check) |
|
257 |
|
258 ;; calendar support |
|
259 ;; |
|
260 ;; This function are used to mark calendar entries directly from the |
|
261 ;; planner files. I try to optimize it with a helper function that |
|
262 ;; take the list of planner-date string corresponding to the three |
|
263 ;; months display of the calendar (same behavior as the |
|
264 ;; mark-diary-entries-hook). One of the helper are made in elisp, and |
|
265 ;; the other used an external perl script to parse the entries. The |
|
266 ;; only thing check is a non-empty (I mean non-space) Events section. |
|
267 ;; Used it as a hook to the `mark-diary-entries-hook'. Since this |
|
268 ;; function also search for diary entries, you can used it in |
|
269 ;; conjunction with diary. And with the `planner-diary-insert' |
|
270 ;; function, you just have to type 'n' at the calendar day to see both |
|
271 ;; the diary and your normal planner entry. The helper script is call events |
|
272 ;; and it's very short (see documentation of planner-mark-calendar-external-helper |
|
273 ;; for the script. The script also return the entry line so that you can remove |
|
274 ;; specially mark entry if you care, either by directly modifying the |
|
275 ;; script or by modifying the planner-mark-calendar-external-helper |
|
276 ;; function. |
|
277 |
|
278 (defcustom planner-mark-calendar-helper 'planner-mark-calendar-internal-helper |
|
279 "Helper function to determinate which day have a non-empty events. |
|
280 Currently, it exist two functions to do it: |
|
281 `planner-mark-calendar-internal-helper' which is a lisp |
|
282 implementation, and `planner-mark-calendar-external-helper' which used |
|
283 a external script to do most of the parsing." |
|
284 :type 'function |
|
285 :group 'planner) |
|
286 |
|
287 (defun planner-mark-calendar () |
|
288 "Look for planner file with non-empty events." |
|
289 (save-window-excursion |
|
290 (set-buffer calendar-buffer) |
|
291 (let ((prev-month displayed-month) |
|
292 (prev-year displayed-year) |
|
293 (succ-month displayed-month) |
|
294 (succ-year displayed-year) |
|
295 (last-day) |
|
296 (day) |
|
297 (files nil)) |
|
298 (increment-calendar-month succ-month succ-year 1) |
|
299 (increment-calendar-month prev-month prev-year -1) |
|
300 (setq day (calendar-absolute-from-gregorian (list prev-month 1 prev-year))) |
|
301 (setq last-day |
|
302 (calendar-absolute-from-gregorian |
|
303 (list succ-month |
|
304 (calendar-last-day-of-month succ-month succ-year) |
|
305 succ-year))) |
|
306 (while (<= day last-day) |
|
307 (let* ((date (calendar-gregorian-from-absolute day)) |
|
308 (file |
|
309 (expand-file-name |
|
310 (calendar-date-to-planner date) |
|
311 planner-directory))) |
|
312 (if (file-readable-p file) |
|
313 (setq files (cons file files)))) |
|
314 (setq day (1+ day))) |
|
315 (setq files (funcall planner-mark-calendar-helper files)) |
|
316 (while files |
|
317 (mark-visible-calendar-date (planner-date-to-calendar (car files))) |
|
318 (setq files (cdr files)))))) |
|
319 |
|
320 (defun planner-mark-calendar-internal-helper (files) |
|
321 "Local (elisp) helper function for `planner-mark-calendar'." |
|
322 (let ((correct)) |
|
323 (while files |
|
324 (find-file-other-window (car files)) |
|
325 (goto-char (point-min)) |
|
326 (if (re-search-forward "^\\* Events" nil t) |
|
327 (progn |
|
328 (forward-line 1) |
|
329 (if (re-search-forward |
|
330 "[^[:space:]]+" |
|
331 (save-excursion |
|
332 (re-search-forward "^\\* " nil t) |
|
333 (let ((p (match-beginning 0))) |
|
334 (and p (1- p)))) |
|
335 t) |
|
336 (setq correct (cons (car files) correct))))) |
|
337 (if (not (buffer-modified-p)) (kill-this-buffer)) |
|
338 (setq files (cdr files))) |
|
339 correct)) |
|
340 |
|
341 (defcustom planner-mark-calendar-external-script "events %s" |
|
342 "Command line that select files that must be marked. |
|
343 This string is send to the shell with %s replace with a list |
|
344 of space separate (maybe non-existing) file names. An example |
|
345 of such script follow: |
|
346 |
|
347 #!/usr/bin/perl -s |
|
348 |
|
349 for $file (sort @ARGV) { |
|
350 open (FILE, $file) || die \"Cannot open file $file\\n\"; |
|
351 $inevents = 0; |
|
352 while (<FILE>) { |
|
353 if ($inevents == 0) { |
|
354 if (/^\\* Events/) { |
|
355 $inevents = 1; |
|
356 } |
|
357 } elsif (/^\\* /) { |
|
358 $inevents = 0; |
|
359 break; |
|
360 } |
|
361 elsif (/^\\s*\\S/) { |
|
362 printf \"$file: $_\"; |
|
363 } |
|
364 } |
|
365 close (FILE); |
|
366 }; |
|
367 " |
|
368 :type 'string |
|
369 :group 'planner) |
|
370 |
|
371 (defun planner-mark-calendar-external-helper (files) |
|
372 "Use an external application to parse the planner files. |
|
373 The command line is set with the `planner-mark-calendar-external-script' |
|
374 variable and must take a list of files as arguments." |
|
375 (let* ((command (concat "events " |
|
376 (mapconcat 'identity files " "))) |
|
377 (output (split-string (shell-command-to-string command) "\n")) |
|
378 (results)) |
|
379 (while output |
|
380 (if (string-match "^\\([^:]+\\):" (car output)) |
|
381 (setq results |
|
382 (cons (match-string 1 (car output)) |
|
383 results))) |
|
384 (setq output (cdr output))) |
|
385 results)) |
|
386 |
|
387 |
|
388 ;; planner-mode key mapping |
|
389 ;; |
|
390 ;; No comment for this one. Do ye want. |
|
391 |
|
392 (eval-after-load "planner" |
|
393 '(progn |
|
394 (define-key planner-mode-map [(control ?c) (control ?w)] |
|
395 'planner-goto-schedule) |
|
396 (define-key planner-mode-map [(control ?c) (control ?n)] |
|
397 'planner-create-note) |
|
398 (define-key planner-mode-map [(control ?c) (control ?e)] |
|
399 'planner-appt-add))) |
|
400 |
|
401 ;; global key mapping |
|
402 |
|
403 (define-key ctl-x-map "ta" 'planner-create-task) |
|
404 (define-key ctl-x-map "ts" 'planner-goto-today) |
|
405 (define-key ctl-x-map "ti" 'timeclock-in) |
|
406 (define-key ctl-x-map "to" 'timeclock-out) |
|
407 (define-key ctl-x-map "tc" 'timeclock-change) |
|
408 (define-key ctl-x-map "tr" 'timeclock-reread-log) |
|
409 (define-key ctl-x-map "tv" 'timeclock-status-string) |
|
410 ;(define-key ctl-x-map "tu" 'timeclock-update-modeline) |
|
411 ;(define-key ctl-x-map "tw" 'timeclock-when-to-leave-string) |
|
412 |
|
413 |
|
414 (defun local-planner-kill-buffer () |
|
415 "Kill the buffer and erase it if empty." |
|
416 (interactive) |
|
417 (let ( |
|
418 (planner-not-empty-file 'kill-this-buffer) |
|
419 ; (planner-not-empty-file (lambda () (save-buffer) (kill-this-buffer))) |
|
420 ) |
|
421 (planner-maybe-remove-file))) |
|
422 |
|
423 ;; planner-mode key mapping |
|
424 (eval-after-load "planner" |
|
425 '(progn |
|
426 (define-key planner-mode-map [(control ?c) (control ?w)] |
|
427 'planner-goto-schedule) |
|
428 (define-key planner-mode-map [(control ?c) (control ?n)] |
|
429 'planner-create-note) |
|
430 (define-key planner-mode-map [(control ?c) (control ?k)] |
|
431 'local-planner-kill-buffer) |
|
432 (define-key planner-mode-map [(control ?c) (control ?e)] |
|
433 'planner-appt-add))) |
|
434 |
|
435 ;; global key mapping |
|
436 |
|
437 (define-key ctl-x-map "ta" 'planner-create-task) |
|
438 (define-key ctl-x-map "ts" 'planner-goto-today) |
|
439 (define-key ctl-x-map "tj" 'planner-goto) |
|
440 (define-key ctl-x-map "ti" 'timeclock-in) |
|
441 (define-key ctl-x-map "to" 'timeclock-out) |
|
442 (define-key ctl-x-map "tc" 'timeclock-change) |
|
443 (define-key ctl-x-map "tr" 'timeclock-reread-log) |
|
444 (define-key ctl-x-map "tv" 'timeclock-status-string) |
|
445 ;(define-key ctl-x-map "tu" 'timeclock-update-modeline) |
|
446 ;(define-key ctl-x-map "tw" 'timeclock-when-to-leave-string) |
|
447 |
|
448 (provide 'local-planner) |