1
|
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)
|